[
  {
    "path": ".config/dotnet-tools.json",
    "content": "{\n  \"version\": 1,\n  \"isRoot\": true,\n  \"tools\": {\n    \"cake.tool\": {\n      \"version\": \"6.1.0\",\n      \"commands\": [\n        \"dotnet-cake\"\n      ]\n    },\n    \"dotnet-reportgenerator-globaltool\": {\n      \"version\": \"5.5.4\",\n      \"commands\": [\n        \"reportgenerator\"\n      ],\n      \"rollForward\": false\n    }\n  }\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "*/*/bin\r\n*/*/obj\r\nartifacts/\r\nTestResults/\r\ntools/"
  },
  {
    "path": ".editorconfig",
    "content": "# Remove the line below if you want to inherit .editorconfig settings from higher directories\r\nroot = true\r\n\r\n# XML files\r\n[*.xml]\r\nindent_style = space\r\nindent_size = 2\r\n\r\n# C# files\r\n[*.cs]\r\n\r\n#### Core EditorConfig Options ####\r\n\r\n# Indentation and spacing\r\nindent_size = 4\r\nindent_style = space\r\ntab_width = 4\r\n\r\n# New line preferences\r\nend_of_line = crlf\r\ninsert_final_newline = true\r\n\r\n#### .NET Coding Conventions ####\r\n\r\n# Organize usings\r\ndotnet_separate_import_directive_groups = false\r\ndotnet_sort_system_directives_first = false\r\nfile_header_template = unset\r\n\r\n# this. and Me. preferences\r\ndotnet_style_qualification_for_event = false\r\ndotnet_style_qualification_for_field = false\r\ndotnet_style_qualification_for_method = false\r\ndotnet_style_qualification_for_property = false\r\n\r\n# Language keywords vs BCL types preferences\r\ndotnet_style_predefined_type_for_locals_parameters_members = true\r\ndotnet_style_predefined_type_for_member_access = true\r\n\r\n# Parentheses preferences\r\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity\r\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity\r\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary\r\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity\r\n\r\n# Modifier preferences\r\ndotnet_style_require_accessibility_modifiers = for_non_interface_members\r\n\r\n# Expression-level preferences\r\ndotnet_style_coalesce_expression = true\r\ndotnet_style_collection_initializer = true\r\ndotnet_style_explicit_tuple_names = true\r\ndotnet_style_namespace_match_folder = true\r\ndotnet_style_null_propagation = true\r\ndotnet_style_object_initializer = true\r\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\r\ndotnet_style_prefer_auto_properties = true\r\ndotnet_style_prefer_collection_expression = false:suggestion\r\ndotnet_style_prefer_compound_assignment = true\r\ndotnet_style_prefer_conditional_expression_over_assignment = true\r\ndotnet_style_prefer_conditional_expression_over_return = true\r\ndotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed\r\ndotnet_style_prefer_inferred_anonymous_type_member_names = true\r\ndotnet_style_prefer_inferred_tuple_names = true\r\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true\r\ndotnet_style_prefer_simplified_boolean_expressions = true\r\ndotnet_style_prefer_simplified_interpolation = true\r\n\r\n# Field preferences\r\ndotnet_style_readonly_field = true\r\n\r\n# Parameter preferences\r\ndotnet_code_quality_unused_parameters = all\r\n\r\n# Suppression preferences\r\ndotnet_remove_unnecessary_suppression_exclusions = none\r\n\r\n# New line preferences\r\ndotnet_style_allow_multiple_blank_lines_experimental = true\r\ndotnet_style_allow_statement_immediately_after_block_experimental = true\r\n\r\n#### C# Coding Conventions ####\r\n\r\n# var preferences\r\ncsharp_style_var_elsewhere = false\r\ncsharp_style_var_for_built_in_types = false\r\ncsharp_style_var_when_type_is_apparent = false\r\n\r\n# Expression-bodied members\r\ncsharp_style_expression_bodied_accessors = true:silent\r\ncsharp_style_expression_bodied_constructors = false:silent\r\ncsharp_style_expression_bodied_indexers = true:silent\r\ncsharp_style_expression_bodied_lambdas = true:silent\r\ncsharp_style_expression_bodied_local_functions = false:silent\r\ncsharp_style_expression_bodied_methods = false:silent\r\ncsharp_style_expression_bodied_operators = false:silent\r\ncsharp_style_expression_bodied_properties = true:silent\r\n\r\n# Pattern matching preferences\r\ncsharp_style_pattern_matching_over_as_with_null_check = true\r\ncsharp_style_pattern_matching_over_is_with_cast_check = true\r\ncsharp_style_prefer_extended_property_pattern = true\r\ncsharp_style_prefer_not_pattern = true\r\ncsharp_style_prefer_pattern_matching = true\r\ncsharp_style_prefer_switch_expression = true\r\n\r\n# Null-checking preferences\r\ncsharp_style_conditional_delegate_call = true\r\n\r\n# Modifier preferences\r\ncsharp_prefer_static_local_function = true\r\ncsharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async\r\ncsharp_style_prefer_readonly_struct = true\r\ncsharp_style_prefer_readonly_struct_member = true\r\n\r\n# Code-block preferences\r\ncsharp_prefer_braces = true:silent\r\ncsharp_prefer_simple_using_statement = true:suggestion\r\ncsharp_style_namespace_declarations = block_scoped:silent\r\ncsharp_style_prefer_method_group_conversion = true:silent\r\ncsharp_style_prefer_primary_constructors = false:suggestion\r\ncsharp_style_prefer_top_level_statements = true:silent\r\n\r\n# Expression-level preferences\r\ncsharp_prefer_simple_default_expression = true\r\ncsharp_style_deconstructed_variable_declaration = true\r\ncsharp_style_implicit_object_creation_when_type_is_apparent = true\r\ncsharp_style_inlined_variable_declaration = true\r\ncsharp_style_prefer_index_operator = true\r\ncsharp_style_prefer_local_over_anonymous_function = true\r\ncsharp_style_prefer_null_check_over_type_check = true\r\ncsharp_style_prefer_range_operator = true\r\ncsharp_style_prefer_tuple_swap = true\r\ncsharp_style_prefer_utf8_string_literals = true\r\ncsharp_style_throw_expression = true\r\ncsharp_style_unused_value_assignment_preference = discard_variable\r\ncsharp_style_unused_value_expression_statement_preference = discard_variable\r\n\r\n# 'using' directive preferences\r\ncsharp_using_directive_placement = outside_namespace:silent\r\n\r\n# New line preferences\r\ncsharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true\r\ncsharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true\r\ncsharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true\r\ncsharp_style_allow_blank_lines_between_consecutive_braces_experimental = true\r\ncsharp_style_allow_embedded_statements_on_same_line_experimental = true\r\n\r\n#### C# Formatting Rules ####\r\n\r\n# New line preferences\r\ncsharp_new_line_before_catch = true\r\ncsharp_new_line_before_else = true\r\ncsharp_new_line_before_finally = true\r\ncsharp_new_line_before_members_in_anonymous_types = true\r\ncsharp_new_line_before_members_in_object_initializers = true\r\ncsharp_new_line_before_open_brace = all\r\ncsharp_new_line_between_query_expression_clauses = true\r\n\r\n# Indentation preferences\r\ncsharp_indent_block_contents = true\r\ncsharp_indent_braces = false\r\ncsharp_indent_case_contents = true\r\ncsharp_indent_case_contents_when_block = true\r\ncsharp_indent_labels = one_less_than_current\r\ncsharp_indent_switch_labels = true\r\n\r\n# Space preferences\r\ncsharp_space_after_cast = false\r\ncsharp_space_after_colon_in_inheritance_clause = true\r\ncsharp_space_after_comma = true\r\ncsharp_space_after_dot = false\r\ncsharp_space_after_keywords_in_control_flow_statements = true\r\ncsharp_space_after_semicolon_in_for_statement = true\r\ncsharp_space_around_binary_operators = before_and_after\r\ncsharp_space_around_declaration_statements = false\r\ncsharp_space_before_colon_in_inheritance_clause = true\r\ncsharp_space_before_comma = false\r\ncsharp_space_before_dot = false\r\ncsharp_space_before_open_square_brackets = false\r\ncsharp_space_before_semicolon_in_for_statement = false\r\ncsharp_space_between_empty_square_brackets = false\r\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\r\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\r\ncsharp_space_between_method_call_parameter_list_parentheses = false\r\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\r\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\r\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\r\ncsharp_space_between_parentheses = false\r\ncsharp_space_between_square_brackets = false\r\n\r\n# Wrapping preferences\r\ncsharp_preserve_single_line_blocks = true\r\ncsharp_preserve_single_line_statements = true\r\n\r\n#### Naming styles ####\r\n\r\n# Naming rules\r\n\r\ndotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion\r\ndotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface\r\ndotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i\r\n\r\ndotnet_naming_rule.types_should_be_pascal_case.severity = suggestion\r\ndotnet_naming_rule.types_should_be_pascal_case.symbols = types\r\ndotnet_naming_rule.types_should_be_pascal_case.style = pascal_case\r\n\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case\r\n\r\n# Symbol specifications\r\n\r\ndotnet_naming_symbols.interface.applicable_kinds = interface\r\ndotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.interface.required_modifiers = \r\n\r\ndotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum\r\ndotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.types.required_modifiers = \r\n\r\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\r\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.non_field_members.required_modifiers = \r\n\r\n# Naming styles\r\n\r\ndotnet_naming_style.pascal_case.required_prefix = \r\ndotnet_naming_style.pascal_case.required_suffix = \r\ndotnet_naming_style.pascal_case.word_separator = \r\ndotnet_naming_style.pascal_case.capitalization = pascal_case\r\n\r\ndotnet_naming_style.begins_with_i.required_prefix = I\r\ndotnet_naming_style.begins_with_i.required_suffix = \r\ndotnet_naming_style.begins_with_i.word_separator = \r\ndotnet_naming_style.begins_with_i.capitalization = pascal_case\r\n\r\n[*.{cs,vb}]\r\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\r\ntab_width = 4\r\nindent_size = 4\r\nend_of_line = crlf\r\ndotnet_style_coalesce_expression = true:suggestion\r\ninsert_final_newline = true\r\ndotnet_style_null_propagation = true:suggestion\r\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion\r\n"
  },
  {
    "path": ".gitattributes",
    "content": "# 2010\r\n*.txt -crlf\r\n\r\n# 2020\r\n*.txt text eol=lf "
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\r\n\r\n## Our Pledge\r\n\r\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\r\n\r\n## Our Standards\r\n\r\nExamples of behavior that contributes to creating a positive environment include:\r\n\r\n* Using welcoming and inclusive language\r\n* Being respectful of differing viewpoints and experiences\r\n* Gracefully accepting constructive criticism\r\n* Focusing on what is best for the community\r\n* Showing empathy towards other community members\r\n\r\nExamples of unacceptable behavior by participants include:\r\n\r\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\r\n* Trolling, insulting/derogatory comments, and personal or political attacks\r\n* Public or private harassment\r\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\r\n* Other conduct which could reasonably be considered inappropriate in a professional setting\r\n\r\n## Our Responsibilities\r\n\r\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\r\n\r\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\r\n\r\n## Scope\r\n\r\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\r\n\r\n## Enforcement\r\n\r\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tom@threemammals.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\r\n\r\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\r\n\r\n## Attribution\r\n\r\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\r\n\r\n[homepage]: http://contributor-covenant.org\r\n[version]: http://contributor-covenant.org/version/1/4/\r\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "We love to receive contributions from the community so please keep them coming :) \r\n\r\nPull requests, issues and commentary welcome!\r\n\r\nPlease complete the relevant template for issues and PRs. Sometimes it's worth getting in touch with us to discuss changes \r\nbefore doing any work incase this is something we are already doing or it might not make sense. We can also give\r\nadvice on the easiest way to do things :)\r\n\r\nFinally we mark all existing issues as help wanted, small, medium and large effort. If you want to contribute for the first time I suggest looking at a help wanted & small effort issue :)\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "## Expected Behavior / New Feature\r\n\r\n\r\n## Actual Behavior / Motivation for New Feature\r\n\r\n\r\n## Steps to Reproduce the Problem\r\n\r\n  1.\r\n  1.\r\n  1.\r\n\r\n## Specifications\r\n\r\n  - Version:\r\n  - Platform:\r\n  - Subsystem:\r\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "Fixes / New Feature #\r\n\r\n## Proposed Changes\r\n\r\n  -\r\n  -\r\n  -\r\n"
  },
  {
    "path": ".github/steps/check-dotnet.sh",
    "content": "#!/bin/bash\n\n# First argument: target .NET major version (digit)\n# Default to 8 if no argument is provided\nDOTNET_VERSION=\"${1:-8}\"\n\n# Check .NET $DOTNET_VERSION\nDOTNET_INFO=$(dotnet --info)\necho Checking for .NET $DOTNET_VERSION SDK in dotnet info output...\necho -------------------------------------------------------------\n\n# Print matching lines\necho \"$DOTNET_INFO\" | grep -E \"^\\s*${DOTNET_VERSION}\\.0\\.[0-9]+\\s+\\[/usr/share/dotnet/sdk\\]\"\n\n# Set environment variable based on match\nif echo \"$DOTNET_INFO\" | grep -qE \"^\\s*${DOTNET_VERSION}\\.0\\.[0-9]+\\s+\\[/usr/share/dotnet/sdk\\]\"; then\n  echo \"DOTNET${DOTNET_VERSION}_installed=true\" >> \"$GITHUB_ENV\"\nelse\n  echo \"DOTNET${DOTNET_VERSION}_installed=false\" >> \"$GITHUB_ENV\"\nfi\n"
  },
  {
    "path": ".github/steps/macos.add-dns-records.sh",
    "content": "#!/bin/bash\nhosts=\"/etc/hosts\"\nif [ ! -f \"$hosts\" ]; then\n  echo \"$hosts not found.\"\n  exit 1\nfi\n\n# Find the line number of the last line that starts with \"##\"\nlast_index=$(grep -n '^##' \"$hosts\" | tail -n 1 | cut -d: -f1)\n\n# Check if the line exists\nif [ -z \"$last_index\" ]; then\n  echo No lines start with '##' in $hosts\n  exit 1\nfi\n\n# Insert DNS-record after the last \"##\" line\nrecord=\"127.0.0.1       threemammals.com\"\n# This 3-line sed script fixes the issue when embedded as a run-action script in GitHub Actions.\n# The problem prevents the workflow file from being parsed correctly, which stops the workflow from starting.\nsudo sed -i '' \"${last_index}a\\\\\n$record\n\" $hosts\n\necho \"Inserted '$record' after line $last_index.\"\necho DNS-record added to $hosts\necho ------------------------\ncat $hosts\necho ------------------------\n\n# The threemammals.com domain is registered for email services\n# https://registrar.ionos.com/domains_raa/whois\n# So, go ahead and clear the DNS cache\nsudo killall -HUP mDNSResponder\n\nping -c 3 threemammals.com\n\n# Additional Loopback IPs\necho -n \"Adding multiple aliases in a loop...\"\nfor i in {2..255}; do\n  sudo ifconfig lo0 alias 127.0.0.$i up\ndone\necho DONE\necho ------------------------\necho Test Loopback IPs\nfor i in {2..255}; do\n  echo ping 127.0.0.$i ...\n  ping -c 1 127.0.0.$i\ndone\n"
  },
  {
    "path": ".github/steps/macos.install-certificate.sh",
    "content": "#!/bin/bash\n# Install mycert2.crt certificate\ncrt='./test/Ocelot.AcceptanceTests/mycert2.crt'\nopenssl version\necho Moving the certificate to the trusted CA store...\nif [ ! -f \"$crt\" ]; then\n  echo \"Certificate file not found: $crt\"\n  exit 1\nfi\ncert_root=\"/Library/Keychains/System.keychain\"\nsudo security add-trusted-cert -d -r trustRoot -k \"$cert_root\" \"$crt\"\necho Certificate added to trusted keychain.\n\necho Verifying installation by listing certificates in $cert_root ...\nsudo security find-certificate -a -c \"threemammals\" -p \"$cert_root\"\n\necho Verifying installation by openssl for $crt in $cert_root ...\n# Export the matching certificate(s) in PEM directly (security find-certificate -p)\ntmpcert=$(mktemp /tmp/mycert.XXXXXX.pem)\n# Use sudo + tee so the redirected file is written with appropriate permissions\nsudo security find-certificate -a -c \"threemammals\" -p \"$cert_root\" | sudo tee \"$tmpcert\" >/dev/null\nif [ ! -s \"$tmpcert\" ]; then\n  echo \"Failed to export certificate to $tmpcert\"\n  rm -f \"$tmpcert\"\n  exit 1\nfi\nchmod 644 \"$tmpcert\"\nopenssl x509 -in \"$tmpcert\" -text -noout\nrm -f \"$tmpcert\"\necho Installation is DONE\n"
  },
  {
    "path": ".github/steps/prepare-coveralls.sh",
    "content": "#!/bin/bash\n# Prepare Coveralls\necho \"::group::Listing environment variables\"\nenv | sort\necho \"::endgroup::\"\n\necho ------------ Detect coverage file ------------ \ncoverage_1st_folder=$(ls -d /home/runner/work/Ocelot/Ocelot/artifacts/UnitTests/*/ | head -1)\necho \"Detected first folder : $coverage_1st_folder\"\ncoverage_file=\"${coverage_1st_folder%/}/coverage.cobertura.xml\"\necho \"Detecting file $coverage_file ...\"\nif [ -f \"$coverage_file\" ]; then\n  echo \"Coverage file exists.\"\n  echo \"COVERALLS_coverage_file_exists=true\" >> $GITHUB_ENV\n  echo \"COVERALLS_coverage_file=$coverage_file\" >> $GITHUB_ENV\nelse\n  echo \"Coverage file DOES NOT exist!\"\n  echo \"COVERALLS_coverage_file_exists=false\" >> $GITHUB_ENV\nfi\n"
  },
  {
    "path": ".github/steps/ubuntu.add-dns-records.sh",
    "content": "#!/bin/bash\nhosts=\"/etc/hosts\"\nif [ ! -f \"$hosts\" ]; then\n  echo \"$hosts not found.\"\n  exit 1\nfi\n\nsudo sed -i '$a 127.0.0.1 threemammals.com' $hosts\n\necho DNS-record added to $hosts\necho ------------------------\ncat $hosts\necho ------------------------\n\nping -c 3 threemammals.com\n"
  },
  {
    "path": ".github/steps/ubuntu.install-certificate.sh",
    "content": "#!/bin/bash\n# Install mycert2.crt certificate\ncrt='./test/Ocelot.AcceptanceTests/mycert2.crt'\nif [ -f \"$crt\" ]; then\n  echo mycert2.crt file found\nfi\n\nopenssl version\n\n# Copy the certificate to the system's trusted CA directory\necho Moving the certificate to the trusted CA store...\ncert='/usr/local/share/ca-certificates/mycert2.crt'\nsudo cp $crt $cert\n\necho Updating the trusted certificates...\nsudo update-ca-certificates # This will add mycert.crt to the trusted root storage\n\necho Verifying installation by listing in /etc/ssl/certs/ folder...\nsudo ls /etc/ssl/certs/ | grep mycert\n\necho Verifying installation by openssl for $cert file...\nsudo chmod 644 $cert # adjusting the permissions\nls -l $cert # verify ownership\nopenssl x509 -in $cert -text -noout\n\necho Installation is DONE\n"
  },
  {
    "path": ".github/steps/windows.add-dns-records.ps1",
    "content": "# Add DNS-records\nWrite-Host \"Hello from PowerShell\"\nGet-Date\n    \n# Append entry to hosts file\nAdd-Content -Path \"$env:SystemRoot\\System32\\drivers\\etc\\hosts\" -Value \"127.0.0.1 threemammals.com\"\n\nWrite-Output \"------------------------\"\nGet-Content \"$env:SystemRoot\\System32\\drivers\\etc\\hosts\"\nWrite-Output \"------------------------\"\n\n# Ping 3 times\nTest-Connection -ComputerName \"threemammals.com\" -Count 3\n"
  },
  {
    "path": ".github/steps/windows.install-certificate.ps1",
    "content": "Write-Host \"Hello from PowerShell\"\nGet-Date\n\n# Install mycert2.crt certificate\n$crt = \".\\test\\Ocelot.AcceptanceTests\\mycert2.crt\"\nif (Test-Path $crt) {\n  Write-Output \"mycert2.crt file found\"\n}\n\nopenssl version\n\nWrite-Output \"Moving the certificate to the trusted CA store...\"\n# Import into the Local Machine Trusted Root Certification Authorities store\n$crt = \".\\test\\Ocelot.AcceptanceTests\\mycert2.crt\"\nImport-Certificate -FilePath $crt -CertStoreLocation Cert:\\LocalMachine\\Root\n\nWrite-Output \"Verifying installation by listing trusted root certificates...\"\n# List certificates in the Trusted Root store and filter for 'mycert'\nGet-ChildItem -Path Cert:\\LocalMachine\\Root | Where-Object { $_.Subject -like \"*threemammals*\" }\n\nWrite-Output \"Verifying installation by openssl for $crt file...\"\n$cert = Get-ChildItem Cert:\\LocalMachine\\Root | Where-Object { $_.Subject -like \"*threemammals*\" }\n$cert_file = \"C:\\temp\\mycert2_installed.cer\"\nExport-Certificate -Cert $cert -FilePath $cert_file\nif (Test-Path $cert_file) {\n  Write-Output \"$cert_file file found\"\n}\n\n# Display certificate details using OpenSSL (if installed)\nopenssl x509 -in $cert_file -text -noout\n\nWrite-Output \"Installation is DONE\"\n"
  },
  {
    "path": ".github/workflows/develop.yml",
    "content": "name: Develop\non:\n  push:\n    branches:\n      - develop\n      # - 'release/**' # for testing purposes\njobs:\n  build-windows:\n    runs-on: windows-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Setup .NET 10\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: 10.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/windows.add-dns-records.ps1\n      - name: Install certificate\n        run: ./.github/steps/windows.install-certificate.ps1\n      - name: Restore\n        run: dotnet restore --locked-mode ./Ocelot.slnx\n      - name: Build\n        run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0\n      - name: Unit Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n      - name: Acceptance Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  build-macos:\n    needs: build-windows\n    runs-on: macos-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: SH-scripts executable\n        run: |\n          chmod +x .github/steps/*.sh\n          ls -l .github/steps/*.sh\n      - name: Setup .NET 10\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: 10.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/macos.add-dns-records.sh\n      - name: Install certificate\n        run: ./.github/steps/macos.install-certificate.sh\n      - name: Restore\n        run: dotnet restore --locked-mode ./Ocelot.slnx\n      - name: Build\n        run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0\n      - name: Unit Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n      - name: Acceptance Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  build-linux:\n    needs: build-macos\n    strategy:\n      matrix:\n        dotnet-version: [ '8', '9', '10' ]\n    runs-on: ubuntu-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v5\n    - name: SH-scripts executable\n      run: |\n        chmod +x .github/steps/*.sh\n        ls -l .github/steps/*.sh\n    - name: Check .NET ${{ matrix.dotnet-version }}\n      id: check-dotnet\n      run: ./.github/steps/check-dotnet.sh ${{ matrix.dotnet-version }}\n    - name: Setup .NET ${{ matrix.dotnet-version }}\n      # if: env.DOTNET${{ matrix.dotnet-version }}_installed == 'false'\n      uses: actions/setup-dotnet@v5\n      with:\n        dotnet-version: ${{ matrix.dotnet-version }}.x\n        cache: true\n        cache-dependency-path: |\n          samples/**/packages.lock.json\n          src/**/packages.lock.json\n          test/**/packages.lock.json\n    - name: .NET Info\n      run: dotnet --info\n    - name: Add DNS-records\n      run: ./.github/steps/ubuntu.add-dns-records.sh  \n    - name: Install certificate\n      run: ./.github/steps/ubuntu.install-certificate.sh\n    - name: Restore\n      run: dotnet restore --locked-mode ./Ocelot.slnx\n    - name: Build\n      run: dotnet build --no-restore ./Ocelot.slnx --framework net${{ matrix.dotnet-version }}.0\n    - name: Unit Tests\n      run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n    - name: Acceptance Tests\n      run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  build-cake:\n    needs: build-linux\n    runs-on: ubuntu-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - name: SH-scripts executable\n        run: chmod +x .github/steps/*.sh\n      - name: Setup .NET 10\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: 10.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: Cake Build\n        uses: cake-build/cake-action@v3\n        with:\n          target: UnitTests # LatestFramework\n      - name: Coverage files\n        run: ./.github/steps/prepare-coveralls.sh\n      - name: Coveralls\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: coverallsapp/github-action@v2\n        with:\n          # fail-on-error: false\n          file: ${{ env.COVERALLS_coverage_file }}\n        continue-on-error: true\n      - name: Codecov\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          slug: ThreeMammals/Ocelot\n          fail_ci_if_error: true\n          files: ${{ env.COVERALLS_coverage_file }}\n          flags: unittests\n        continue-on-error: true\n"
  },
  {
    "path": ".github/workflows/pr-closed.yml",
    "content": "name: PR Closed\non:\n  pull_request:\n    types: [closed]\n\njobs:\n  cleanup:\n    runs-on: ubuntu-latest\n    env:\n      GH_TOKEN: ${{ github.token }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Delete caches via gh CLI\n        run: |\n          pr_no=\"${{ github.event.pull_request.number }}\"\n          echo Pull request $pr_no cache items to be deleted...\n          gh cache list --ref refs/pull/$pr_no/merge          \n          echo --------------------------------------\n          echo All cache items...\n          gh cache list\n          echo --------------------------------------\n          # gh cache list --ref refs/pull/$pr_no/merge --json id --jq '.[].id' | xargs -n1 gh cache delete\n          # Sometimes, items produced by other workflows get reused in the PR workflow\n          # gh cache delete --all --succeed-on-no-caches\n          echo \"PR $pr_no: deleting caches for refs/pull/$pr_no/merge...\"\n          ids=$(gh cache list --ref refs/pull/$pr_no/merge --json id --jq '.[].id')\n          if [ -z \"$ids\" ]; then\n            echo \"No PR caches found.\"\n          else\n            echo \"$ids\" | xargs -r -I{} sh -c 'gh cache delete {} || echo \"delete {} failed (ignored)\"'\n          fi\n          echo DONE\n        continue-on-error: true\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "# This workflow will build a .NET project\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net\nname: PR\non: pull_request\n\njobs:\n#   build-linux:\n#     runs-on: ubuntu-latest\n#     continue-on-error: true\n#     env:\n#       # https://github.com/actions/setup-dotnet/blob/main/README.md#environment-variables\n#       DOTNET_INSTALL_DIR: \"/usr/share/dotnet\" # don't override by /usr/lib/dotnet \n#       NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n#     steps:\n#     - name: Checkout\n#       uses: actions/checkout@v5\n#     - name: SH-scripts executable\n#       run: |\n#         chmod +x .github/steps/*.sh\n#         ls -l .github/steps/*.sh\n#     - name: Check .NET 10\n#       id: check-dotnet10\n#       run: ./.github/steps/check-dotnet.sh 10\n#     - name: Setup .NET 10\n#       # if: env.DOTNET10_installed == 'false'\n#       uses: actions/setup-dotnet@v5\n#       with:\n#         dotnet-version: 10.x\n#         cache: true\n#         cache-dependency-path: |\n#           samples/**/packages.lock.json\n#           src/**/packages.lock.json\n#           test/**/packages.lock.json\n#     - name: .NET Info\n#       run: dotnet --info\n#     - name: Add DNS-records\n#       run: ./.github/steps/ubuntu.add-dns-records.sh  \n#     - name: Install certificate\n#       run: ./.github/steps/ubuntu.install-certificate.sh\n#     - name: Restore\n#       run: dotnet restore --locked-mode ./Ocelot.slnx\n#     - name: Build\n#       run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0\n#     - name: Unit Tests\n#       run: dotnet test --no-restore --no-build --verbosity normal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n#     - name: Acceptance Tests\n#       run: dotnet test --no-restore --no-build --verbosity normal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n#   build-macos:\n#     runs-on: macos-latest\n#     continue-on-error: true\n#     env:\n#       NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n#     steps:\n#       - name: Checkout\n#         uses: actions/checkout@v5\n#       - name: SH-scripts executable\n#         run: |\n#           chmod +x .github/steps/*.sh\n#           ls -l .github/steps/*.sh\n#       - name: Setup .NET 10\n#         uses: actions/setup-dotnet@v5\n#         with:\n#           dotnet-version: 10.x\n#           cache: true\n#           cache-dependency-path: |\n#             samples/**/packages.lock.json\n#             src/**/packages.lock.json\n#             test/**/packages.lock.json\n#       - name: .NET Info\n#         run: dotnet --info\n#       - name: Add DNS-records\n#         run: ./.github/steps/macos.add-dns-records.sh  \n#       - name: Install certificate\n#         run: ./.github/steps/macos.install-certificate.sh\n#       - name: Restore\n#         run: dotnet restore --locked-mode ./Ocelot.slnx\n#       - name: Build\n#         run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0\n#       - name: Unit Tests\n#         run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n#       - name: Acceptance Tests\n#         run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n#   build-windows:\n#     runs-on: windows-latest\n#     continue-on-error: true\n#     env:\n#       NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n#     steps:\n#       - name: Checkout\n#         uses: actions/checkout@v5\n#       - name: Setup .NET 10\n#         uses: actions/setup-dotnet@v5\n#         with:\n#           dotnet-version: 10.x\n#           cache: true\n#           cache-dependency-path: |\n#             samples/**/packages.lock.json\n#             src/**/packages.lock.json\n#             test/**/packages.lock.json\n#       - name: .NET Info\n#         run: dotnet --info\n#       - name: Add DNS-records\n#         run: ./.github/steps/windows.add-dns-records.ps1  \n#       - name: Install certificate\n#         run: ./.github/steps/windows.install-certificate.ps1\n#       - name: Restore\n#         run: dotnet restore --locked-mode ./Ocelot.slnx\n#       - name: Build\n#         run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0\n#       - name: Unit Tests\n#         run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n#       - name: Acceptance Tests\n#         run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n#   build-success:\n#     needs: [build-linux, build-macos, build-windows]\n#     runs-on: ubuntu-latest\n#     if: ${{ always() }} # run even Linux / MacOS / Windows build fails\n#     steps:\n#       - name: Decide success\n#         run: |\n#           linux_result=\"${{ needs.build-linux.result }}\"\n#           macos_result=\"${{ needs.build-macos.result }}\"\n#           windows_result=\"${{ needs.build-windows.result }}\"\n#           if [[ $linux_result == \"success\" || $macos_result == \"success\" || $windows_result == \"success\" ]]; then\n#             echo \"At least one succeeded\"\n#           else\n#             echo \"All failed\"\n#             exit 1\n#           fi\n\n  build-cake:\n    runs-on: ubuntu-latest\n    # environment: Pull-Request\n    env:\n      # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - name: SH-scripts executable\n        run: |\n          chmod +x .github/steps/*.sh\n          ls -l .github/steps/*.sh\n      - name: Check .NET 10\n        id: check-dotnet10\n        run: ./.github/steps/check-dotnet.sh 10\n      - name: Setup .NET 10\n        # if: env.DOTNET10_installed == 'false'\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: 10.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/ubuntu.add-dns-records.sh  \n      - name: Install certificate\n        run: ./.github/steps/ubuntu.install-certificate.sh\n      - name: Cake Build\n        uses: cake-build/cake-action@v3\n        with:\n          target: PullRequest\n          verbosity: Verbose\n      - name: Coverage files\n        run: ./.github/steps/prepare-coveralls.sh\n      - name: Coveralls\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: coverallsapp/github-action@v2\n        with:\n          # fail-on-error: false\n          file: ${{ env.COVERALLS_coverage_file }}\n        continue-on-error: true\n      - name: Codecov\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          slug: ThreeMammals/Ocelot\n          fail_ci_if_error: true\n          files: ${{ env.COVERALLS_coverage_file }}\n          flags: unittests\n        continue-on-error: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    branches:\n      - main\n      - 'release/24.**'\njobs:\n  build-windows:\n    strategy:\n      matrix:\n        dotnet-version: [ '8', '9', '10' ]\n    runs-on: windows-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Setup .NET ${{ matrix.dotnet-version }}\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: ${{ matrix.dotnet-version }}.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/windows.add-dns-records.ps1\n      - name: Install certificate\n        run: ./.github/steps/windows.install-certificate.ps1\n      - name: Restore\n        run: dotnet restore --locked-mode ./Ocelot.Release.sln\n      - name: Build\n        run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0\n      - name: Unit Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n      - name: Acceptance Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  build-macos:\n    needs: build-windows\n    strategy:\n      matrix:\n        dotnet-version: [ '8', '9', '10' ]\n    runs-on: macos-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: SH-scripts executable\n        run: |\n          chmod +x .github/steps/*.sh\n          ls -l .github/steps/*.sh\n      - name: Setup .NET ${{ matrix.dotnet-version }}\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: ${{ matrix.dotnet-version }}.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/macos.add-dns-records.sh\n      - name: Install certificate\n        run: ./.github/steps/macos.install-certificate.sh\n      - name: Restore\n        run: dotnet restore --locked-mode ./Ocelot.Release.sln\n      - name: Build\n        run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0\n      - name: Unit Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n      - name: Acceptance Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  build-linux:\n    needs: build-macos\n    strategy:\n      matrix:\n        dotnet-version: [ '8', '9', '10' ]\n    runs-on: ubuntu-latest\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: SH-scripts executable\n        run: |\n          chmod +x .github/steps/*.sh\n          ls -l .github/steps/*.sh\n      - name: Check .NET ${{ matrix.dotnet-version }}\n        id: check-dotnet\n        run: ./.github/steps/check-dotnet.sh ${{ matrix.dotnet-version }}\n      - name: Setup .NET ${{ matrix.dotnet-version }}\n        # if: env.DOTNET${{ matrix.dotnet-version }}_installed == 'false'\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: ${{ matrix.dotnet-version }}.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: .NET Info\n        run: dotnet --info\n      - name: Add DNS-records\n        run: ./.github/steps/ubuntu.add-dns-records.sh  \n      - name: Install certificate\n        run: ./.github/steps/ubuntu.install-certificate.sh\n      - name: Restore\n        run: dotnet restore --locked-mode ./Ocelot.Release.sln\n      - name: Build\n        run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0\n      - name: Unit Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\n      - name: Acceptance Tests\n        run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\n\n  release-cake:\n    needs: build-linux\n    runs-on: ubuntu-latest\n    environment: build.cake # TODO Rename to Release\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n    steps:\n      - name: Env Variables\n        env:\n          # CAKE_RELEASE_MYVAR: ${{ vars.CAKE_RELEASE_MYVAR }}\n          # TEMP_KEY: ${{ secrets.TEMP_KEY }} # leaked secret LoL\n          GITHUB_CONTEXT: ${{ toJson(github) }}\n        run: |\n          echo \"github context >>>\" # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context\n          echo \"$GITHUB_CONTEXT\"\n          echo \"<<<\"\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - name: SH-scripts executable\n        run: |\n          chmod +x .github/steps/*.sh\n          ls -l .github/steps/*.sh\n      - name: Check .NET 10\n        id: check-dotnet\n        run: ./.github/steps/check-dotnet.sh 10\n      - name: Setup .NET 10\n        uses: actions/setup-dotnet@v5\n        with:\n          dotnet-version: 10.x\n          cache: true\n          cache-dependency-path: |\n            samples/**/packages.lock.json\n            src/**/packages.lock.json\n            test/**/packages.lock.json\n      - name: Cake Build\n        uses: cake-build/cake-action@v3\n        with:\n          target: Release\n        env:\n          OCELOT_GITHUB_API_KEY: ${{ secrets.OCELOT_GITHUB_API_KEY }}\n          OCELOT_NUGET_API_KEY_2025: ${{ secrets.OCELOT_NUGET_API_KEY_2025 }}\n          # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n      - name: Coverage files\n        run: ./.github/steps/prepare-coveralls.sh\n      - name: Coveralls\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: coverallsapp/github-action@v2\n        with:\n          file: ${{ env.COVERALLS_coverage_file }}\n          compare-ref: main\n        continue-on-error: true\n      - name: Codecov\n        if: env.COVERALLS_coverage_file_exists == 'true'\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          slug: ThreeMammals/Ocelot\n          fail_ci_if_error: true\n          files: ${{ env.COVERALLS_coverage_file }}\n          flags: unittests\n        continue-on-error: true\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\r\n## files generated by popular Visual Studio add-ons.\r\n##\r\n## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore\r\n\r\n# User-specific files\r\n*.rsuser\r\n*.suo\r\n*.user\r\n*.userosscache\r\n*.sln.docstates\r\n\r\n# User-specific files (MonoDevelop/Xamarin Studio)\r\n*.userprefs\r\n\r\n# Mono auto generated files\r\nmono_crash.*\r\n\r\n# Build results\r\n[Dd]ebug/\r\n[Dd]ebugPublic/\r\n[Rr]elease/\r\n[Rr]eleases/\r\nx64/\r\nx86/\r\n[Ww][Ii][Nn]32/\r\n[Aa][Rr][Mm]/\r\n[Aa][Rr][Mm]64/\r\nbld/\r\n[Bb]in/\r\n[Oo]bj/\r\n[Ll]og/\r\n[Ll]ogs/\r\n\r\n# Visual Studio 2015/2017 cache/options directory\r\n.vs/\r\n# Uncomment if you have tasks that create the project's static files in wwwroot\r\n#wwwroot/\r\n\r\n# Visual Studio 2017 auto generated files\r\nGenerated\\ Files/\r\n\r\n# MSTest test Results\r\n[Tt]est[Rr]esult*/\r\n[Bb]uild[Ll]og.*\r\n\r\n# NUnit\r\n*.VisualState.xml\r\nTestResult.xml\r\nnunit-*.xml\r\n\r\n# Build Results of an ATL Project\r\n[Dd]ebugPS/\r\n[Rr]eleasePS/\r\ndlldata.c\r\n\r\n# Benchmark Results\r\nBenchmarkDotNet.Artifacts/\r\n\r\n# .NET Core\r\nproject.lock.json\r\nproject.fragment.lock.json\r\nartifacts/\r\n\r\n# ASP.NET Scaffolding\r\nScaffoldingReadMe.txt\r\n\r\n# StyleCop\r\nStyleCopReport.xml\r\n\r\n# Files built by Visual Studio\r\n*_i.c\r\n*_p.c\r\n*_h.h\r\n*.ilk\r\n*.meta\r\n*.obj\r\n*.iobj\r\n*.pch\r\n*.pdb\r\n*.ipdb\r\n*.pgc\r\n*.pgd\r\n*.rsp\r\n*.sbr\r\n*.tlb\r\n*.tli\r\n*.tlh\r\n*.tmp\r\n*.tmp_proj\r\n*_wpftmp.csproj\r\n*.log\r\n*.tlog\r\n*.vspscc\r\n*.vssscc\r\n.builds\r\n*.pidb\r\n*.svclog\r\n*.scc\r\n\r\n# Chutzpah Test files\r\n_Chutzpah*\r\n\r\n# Visual C++ cache files\r\nipch/\r\n*.aps\r\n*.ncb\r\n*.opendb\r\n*.opensdf\r\n*.sdf\r\n*.cachefile\r\n*.VC.db\r\n*.VC.VC.opendb\r\n\r\n# Visual Studio profiler\r\n*.psess\r\n*.vsp\r\n*.vspx\r\n*.sap\r\n\r\n# Visual Studio Trace Files\r\n*.e2e\r\n\r\n# TFS 2012 Local Workspace\r\n$tf/\r\n\r\n# Guidance Automation Toolkit\r\n*.gpState\r\n\r\n# ReSharper is a .NET coding add-in\r\n_ReSharper*/\r\n*.[Rr]e[Ss]harper\r\n*.DotSettings.user\r\n\r\n# TeamCity is a build add-in\r\n_TeamCity*\r\n\r\n# DotCover is a Code Coverage Tool\r\n*.dotCover\r\n\r\n# AxoCover is a Code Coverage Tool\r\n.axoCover/*\r\n!.axoCover/settings.json\r\n\r\n# Coverlet is a free, cross platform Code Coverage Tool\r\ncoverage*.json\r\ncoverage*.xml\r\ncoverage*.info\r\n\r\n# Visual Studio code coverage results\r\n*.coverage\r\n*.coveragexml\r\n\r\n# NCrunch\r\n_NCrunch_*\r\n.*crunch*.local.xml\r\nnCrunchTemp_*\r\n\r\n# MightyMoose\r\n*.mm.*\r\nAutoTest.Net/\r\n\r\n# Web workbench (sass)\r\n.sass-cache/\r\n\r\n# Installshield output folder\r\n[Ee]xpress/\r\n\r\n# DocProject is a documentation generator add-in\r\nDocProject/buildhelp/\r\nDocProject/Help/*.HxT\r\nDocProject/Help/*.HxC\r\nDocProject/Help/*.hhc\r\nDocProject/Help/*.hhk\r\nDocProject/Help/*.hhp\r\nDocProject/Help/Html2\r\nDocProject/Help/html\r\n\r\n# Click-Once directory\r\npublish/\r\n\r\n# Publish Web Output\r\n*.[Pp]ublish.xml\r\n*.azurePubxml\r\n# Note: Comment the next line if you want to checkin your web deploy settings,\r\n# but database connection strings (with potential passwords) will be unencrypted\r\n*.pubxml\r\n*.publishproj\r\n\r\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\r\n# checkin your Azure Web App publish settings, but sensitive information contained\r\n# in these scripts will be unencrypted\r\nPublishScripts/\r\n\r\n# NuGet Packages\r\n*.nupkg\r\n# NuGet Symbol Packages\r\n*.snupkg\r\n# The packages folder can be ignored because of Package Restore\r\n**/[Pp]ackages/*\r\n# except build/, which is used as an MSBuild target.\r\n!**/[Pp]ackages/build/\r\n# Uncomment if necessary however generally it will be regenerated when needed\r\n#!**/[Pp]ackages/repositories.config\r\n# NuGet v3's project.json files produces more ignorable files\r\n*.nuget.props\r\n*.nuget.targets\r\n\r\n# Microsoft Azure Build Output\r\ncsx/\r\n*.build.csdef\r\n\r\n# Microsoft Azure Emulator\r\necf/\r\nrcf/\r\n\r\n# Windows Store app package directories and files\r\nAppPackages/\r\nBundleArtifacts/\r\nPackage.StoreAssociation.xml\r\n_pkginfo.txt\r\n*.appx\r\n*.appxbundle\r\n*.appxupload\r\n\r\n# Visual Studio cache files\r\n# files ending in .cache can be ignored\r\n*.[Cc]ache\r\n# but keep track of directories ending in .cache\r\n!?*.[Cc]ache/\r\n\r\n# Others\r\nClientBin/\r\n~$*\r\n*~\r\n*.dbmdl\r\n*.dbproj.schemaview\r\n*.jfm\r\n*.pfx\r\n*.publishsettings\r\norleans.codegen.cs\r\n\r\n# Including strong name files can present a security risk\r\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\r\n#*.snk\r\n\r\n# Since there are multiple workflows, uncomment next line to ignore bower_components\r\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\r\n#bower_components/\r\n\r\n# RIA/Silverlight projects\r\nGenerated_Code/\r\n\r\n# Backup & report files from converting an old project file\r\n# to a newer Visual Studio version. Backup files are not needed,\r\n# because we have git ;-)\r\n_UpgradeReport_Files/\r\nBackup*/\r\nUpgradeLog*.XML\r\nUpgradeLog*.htm\r\nServiceFabricBackup/\r\n*.rptproj.bak\r\n\r\n# SQL Server files\r\n*.mdf\r\n*.ldf\r\n*.ndf\r\n\r\n# Business Intelligence projects\r\n*.rdl.data\r\n*.bim.layout\r\n*.bim_*.settings\r\n*.rptproj.rsuser\r\n*- [Bb]ackup.rdl\r\n*- [Bb]ackup ([0-9]).rdl\r\n*- [Bb]ackup ([0-9][0-9]).rdl\r\n\r\n# Microsoft Fakes\r\nFakesAssemblies/\r\n\r\n# GhostDoc plugin setting file\r\n*.GhostDoc.xml\r\n\r\n# Node.js Tools for Visual Studio\r\n.ntvs_analysis.dat\r\nnode_modules/\r\n\r\n# Visual Studio 6 build log\r\n*.plg\r\n\r\n# Visual Studio 6 workspace options file\r\n*.opt\r\n\r\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\r\n*.vbw\r\n\r\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\r\n*.vbp\r\n\r\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\r\n*.dsw\r\n*.dsp\r\n\r\n# Visual Studio 6 technical files\r\n*.ncb\r\n*.aps\r\n\r\n# Visual Studio LightSwitch build output\r\n**/*.HTMLClient/GeneratedArtifacts\r\n**/*.DesktopClient/GeneratedArtifacts\r\n**/*.DesktopClient/ModelManifest.xml\r\n**/*.Server/GeneratedArtifacts\r\n**/*.Server/ModelManifest.xml\r\n_Pvt_Extensions\r\n\r\n# Paket dependency manager\r\n.paket/paket.exe\r\npaket-files/\r\n\r\n# FAKE - F# Make\r\n.fake/\r\n\r\n# CodeRush personal settings\r\n.cr/personal\r\n\r\n# Python Tools for Visual Studio (PTVS)\r\n__pycache__/\r\n*.pyc\r\n\r\n# Cake - Uncomment if you are using it\r\n# tools/**\r\n# !tools/packages.config\r\n\r\n# Tabs Studio\r\n*.tss\r\n\r\n# Telerik's JustMock configuration file\r\n*.jmconfig\r\n\r\n# BizTalk build output\r\n*.btp.cs\r\n*.btm.cs\r\n*.odx.cs\r\n*.xsd.cs\r\n\r\n# OpenCover UI analysis results\r\nOpenCover/\r\n\r\n# Azure Stream Analytics local run output\r\nASALocalRun/\r\n\r\n# MSBuild Binary and Structured Log\r\n*.binlog\r\n\r\n# NVidia Nsight GPU debugger configuration file\r\n*.nvuser\r\n\r\n# MFractors (Xamarin productivity tool) working folder\r\n.mfractor/\r\n\r\n# Local History for Visual Studio\r\n.localhistory/\r\n\r\n# Visual Studio History (VSHistory) files\r\n.vshistory/\r\n\r\n# BeatPulse healthcheck temp database\r\nhealthchecksdb\r\n\r\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\r\nMigrationBackup/\r\n\r\n# Ionide (cross platform F# VS Code tools) working folder\r\n.ionide/\r\n\r\n# Fody - auto-generated XML schema\r\nFodyWeavers.xsd\r\n\r\n# VS Code files for those working on multiple tools\r\n.vscode/*\r\n!.vscode/settings.json\r\n!.vscode/tasks.json\r\n!.vscode/launch.json\r\n!.vscode/extensions.json\r\n*.code-workspace\r\n\r\n# Local History for Visual Studio Code\r\n.history/\r\n\r\n# Windows Installer files from build outputs\r\n*.cab\r\n*.msi\r\n*.msix\r\n*.msm\r\n*.msp\r\n\r\n# JetBrains Rider\r\n*.sln.iml\r\n.idea/\r\n\r\n# Visual Studio Test Results\r\n# Quick Facts: https://file.org/extension/trx#visualstudiotestresults\r\n# Running automated tests from the command line: https://learn.microsoft.com/en-us/previous-versions/ms182486(v=vs.140)\r\n# Run unit tests with Test Explorer: https://learn.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer\r\n*.trx\r\n\r\n# OCELOT\r\n\r\n# FAKE - F# Make\r\n.fake/\r\n!tools/packages.config\r\ntools/\r\n\r\n# ReportGenerator dotnet tool -> https://reportgenerator.io/\r\n**/coveragereport/\r\n\r\n# Ocelot acceptance test config\r\ntest/Ocelot.AcceptanceTests/ocelot.json\r\n\r\n# Read the Docs -> https://ocelot.readthedocs.io\r\ndocs/_build/\r\ndocs/_templates/\r\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-24.04\n  tools:\n    python: \"latest\"\n    # You can also specify other tool versions:\n    # nodejs: \"19\"\n    # rust: \"1.64\"\n    # golang: \"1.19\"\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n   configuration: docs/conf.py\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: docs/requirements.txt\n"
  },
  {
    "path": "GitVersion.yml",
    "content": "mode: ContinuousDelivery\r\nbranches: {}\r\nignore:\r\n  sha: []\r\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT), Open Source Software,\r\nCopyright © 2016-2026 Tom Gardham-Pallister, Raman Maksimchuk and contributors.\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy of this open-source software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r\n"
  },
  {
    "path": "Ocelot.Samples.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 18\nVisualStudioVersion = 18.3.11520.95 d18.3\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot\", \"src\\Ocelot\\Ocelot.csproj\", \"{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.Eureka.ApiGateway\", \"samples\\Eureka\\ApiGateway\\Ocelot.Samples.Eureka.ApiGateway.csproj\", \"{EA0E146F-2C2B-4176-B6EC-F62A587F5077}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.Eureka.DownstreamService\", \"samples\\Eureka\\DownstreamService\\Ocelot.Samples.Eureka.DownstreamService.csproj\", \"{B7317B64-2208-472D-90AC-F42B61956B79}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.GraphQL\", \"samples\\GraphQL\\Ocelot.Samples.GraphQL.csproj\", \"{6CCA3677-420A-4294-8D41-67CF3D818575}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.Kubernetes.ApiGateway\", \"samples\\Kubernetes\\ApiGateway\\Ocelot.Samples.Kubernetes.ApiGateway.csproj\", \"{721C1737-70CB-4B11-A19B-C7AAC6856CC7}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.Kubernetes.DownstreamService\", \"samples\\Kubernetes\\DownstreamService\\Ocelot.Samples.Kubernetes.DownstreamService.csproj\", \"{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceDiscovery.ApiGateway\", \"samples\\ServiceDiscovery\\ApiGateway\\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj\", \"{96B9F16E-C95D-425A-A419-40CB3C90CB77}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceDiscovery.DownstreamService\", \"samples\\ServiceDiscovery\\DownstreamService\\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj\", \"{60E14B1A-C295-453B-910E-58E09F5A28AA}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceFabric.ApiGateway\", \"samples\\ServiceFabric\\ApiGateway\\Ocelot.Samples.ServiceFabric.ApiGateway.csproj\", \"{115F7934-3326-492A-B131-64F0EAEBAD71}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceFabric.DownstreamService\", \"samples\\ServiceFabric\\DownstreamService\\Ocelot.Samples.ServiceFabric.DownstreamService.csproj\", \"{6C777A20-F557-45CF-B87B-11E3C6B29A36}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.Web\", \"samples\\Web\\Ocelot.Samples.Web.csproj\", \"{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.Basic\", \"samples\\Basic\\Ocelot.Samples.Basic.csproj\", \"{3225BD01-42ED-4362-9757-28CBFF6B2D70}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.Configuration\", \"samples\\Configuration\\Ocelot.Samples.Configuration.csproj\", \"{FE091795-7FBF-4D82-ABD6-51405F210142}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.Metadata\", \"samples\\Metadata\\Ocelot.Samples.Metadata.csproj\", \"{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.ActiveCfg = Debug|x64\n\t\t{115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.Build.0 = Debug|x64\n\t\t{115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3225BD01-42ED-4362-9757-28CBFF6B2D70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3225BD01-42ED-4362-9757-28CBFF6B2D70}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3225BD01-42ED-4362-9757-28CBFF6B2D70}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3225BD01-42ED-4362-9757-28CBFF6B2D70}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{FE091795-7FBF-4D82-ABD6-51405F210142}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{FE091795-7FBF-4D82-ABD6-51405F210142}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{FE091795-7FBF-4D82-ABD6-51405F210142}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{FE091795-7FBF-4D82-ABD6-51405F210142}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {2C1620D4-EB38-4C3E-9FC5-029FB6B2F426}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "Ocelot.Samples.slnx",
    "content": "<Solution>\n  <Project Path=\"samples/Basic/Ocelot.Samples.Basic.csproj\" />\n  <Project Path=\"samples/Configuration/Ocelot.Samples.Configuration.csproj\" />\n  <Project Path=\"samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj\" />\n  <Project Path=\"samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj\" />\n  <Project Path=\"samples/GraphQL/Ocelot.Samples.GraphQL.csproj\" />\n  <Project Path=\"samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj\" />\n  <Project Path=\"samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj\" />\n  <Project Path=\"samples/Metadata/Ocelot.Samples.Metadata.csproj\" />\n  <Project Path=\"samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj\" />\n  <Project Path=\"samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj\" />\n  <Project Path=\"samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj\" />\n  <Project Path=\"samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj\" />\n  <Project Path=\"samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj\" />\n  <Project Path=\"samples/Web/Ocelot.Samples.Web.csproj\" />\n  <Project Path=\"src/Ocelot/Ocelot.csproj\" />\n</Solution>\n"
  },
  {
    "path": "Ocelot.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 18\nVisualStudioVersion = 18.3.11520.95\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t.dockerignore = .dockerignore\n\t\t.editorconfig = .editorconfig\n\t\t.gitignore = .gitignore\n\t\t.readthedocs.yaml = .readthedocs.yaml\n\t\tbuild.cake = build.cake\n\t\tcodeanalysis.ruleset = codeanalysis.ruleset\n\t\tGitVersion.yml = GitVersion.yml\n\t\tLICENSE.md = LICENSE.md\n\t\tREADME.md = README.md\n\t\tReleaseNotes.md = ReleaseNotes.md\n\tEndProjectSection\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Testing\", \"testing\\Ocelot.Testing.csproj\", \"{D9DF2863-0608-4BBC-8D83-614E2894AF49}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.AcceptanceTests\", \"test\\Ocelot.AcceptanceTests\\Ocelot.AcceptanceTests.csproj\", \"{AA97C983-5A39-D35D-0274-08247BB08FAC}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Benchmarks\", \"test\\Ocelot.Benchmarks\\Ocelot.Benchmarks.csproj\", \"{075F01AD-8478-3463-2BB9-D04EE5C98321}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.ManualTest\", \"test\\Ocelot.ManualTest\\Ocelot.ManualTest.csproj\", \"{DE9F1F3D-7687-9248-E4D8-5684370E185F}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.UnitTests\", \"test\\Ocelot.UnitTests\\Ocelot.UnitTests.csproj\", \"{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot\", \"src\\Ocelot\\Ocelot.csproj\", \"{AB0A194F-36A2-D62B-51FD-44335BE12D1B}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Provider.Consul\", \"src\\Ocelot.Provider.Consul\\Ocelot.Provider.Consul.csproj\", \"{F38B5372-D141-3A0D-6FF8-30EFA93C7506}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Provider.Eureka\", \"src\\Ocelot.Provider.Eureka\\Ocelot.Provider.Eureka.csproj\", \"{3C77D0A0-173A-628F-FA3C-E113B9501E89}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Provider.Kubernetes\", \"src\\Ocelot.Provider.Kubernetes\\Ocelot.Provider.Kubernetes.csproj\", \"{1FD9E0B6-EC08-54E4-82F2-A215A388968D}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Provider.Polly\", \"src\\Ocelot.Provider.Polly\\Ocelot.Provider.Polly.csproj\", \"{1424441E-1143-0F05-1346-E90195CDE10E}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tDebug|x64 = Debug|x64\n\t\tDebug|x86 = Debug|x86\n\t\tRelease|Any CPU = Release|Any CPU\n\t\tRelease|x64 = Release|x64\n\t\tRelease|x86 = Release|x86\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x64.Build.0 = Release|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x86.Build.0 = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x64.Build.0 = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x86.Build.0 = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x64.Build.0 = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x86.Build.0 = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x64.Build.0 = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x86.Build.0 = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x64.Build.0 = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x86.Build.0 = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x64.Build.0 = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x86.Build.0 = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x64.Build.0 = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x86.Build.0 = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x64.Build.0 = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x86.Build.0 = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x64.Build.0 = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x86.Build.0 = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|x64.Build.0 = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{1424441E-1143-0F05-1346-E90195CDE10E}.Release|x86.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "Ocelot.slnx",
    "content": "<Solution>\n  <Project Path=\"src/Ocelot/Ocelot.csproj\" />\n  <Project Path=\"src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj\" />\n  <Project Path=\"src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj\" />\n  <Project Path=\"src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj\" />\n  <Project Path=\"src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj\" />\n  <Project Path=\"test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\" />\n  <Project Path=\"test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj\" />\n  <Project Path=\"test/Ocelot.ManualTest/Ocelot.ManualTest.csproj\" />\n  <Project Path=\"test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\" />\n  <Project Path=\"testing/Ocelot.Testing.csproj\" />\n</Solution>\n"
  },
  {
    "path": "README.md",
    "content": "﻿![Ocelot Logo](https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/ocelot_logo.png)\r\n\r\n[![Release Status](https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml)\r\n[![Development Status](https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml)\r\n[![ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square)](https://app.readthedocs.org/projects/ocelot/builds/?version__slug=develop)\r\n[![coveralls](https://img.shields.io/coveralls/github/ThreeMammals/Ocelot/develop?label=coveralls&logo=coveralls&logoColor=white)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop)\r\n[![codecov](https://codecov.io/gh/ThreeMammals/Ocelot/branch/develop/graph/badge.svg)](https://codecov.io/gh/ThreeMammals/Ocelot)\r\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ThreeMammals/Ocelot/blob/main/LICENSE.md)\r\n[![NuGet](https://img.shields.io/nuget/v/Ocelot?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/Ocelot/ \"Download Ocelot from NuGet.org\")\r\n[![Downloads](https://img.shields.io/nuget/dt/Ocelot?logo=nuget&label=Downloads)](https://www.nuget.org/packages/Ocelot/ \"Total Ocelot downloads from NuGet.org\")\r\n<!-- [![Coveralls](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=develop)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop) -->\r\n\r\n[~docspassing]: https://img.shields.io/badge/Docs-passing-44CC11?style=flat-square\r\n[~docsfailing]: https://img.shields.io/badge/Docs-failing-red?style=flat-square\r\n\r\n## About\r\nOcelot is a .NET [API gateway](https://www.bing.com/search?q=API+gateway).\r\nThis project is aimed at people using .NET running a microservices (service-oriented) architecture that needs a unified point of entry into their system.\r\nHowever, it will work with anything that speaks HTTP(S) and runs on any platform that [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/) supports.\r\n\r\n<!--\r\nIn particular we want easy integration with [IdentityServer](https://github.com/IdentityServer) reference and [Bearer](https://oauth.net/2/bearer-tokens/) tokens. \r\nWe have been unable to find this in our current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens.\r\nWe would rather use the IdentityServer code that already exists to do this.\r\n-->\r\n\r\nOcelot consists of a series of ASP.NET Core [middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/) arranged in a specific order.\r\nOcelot [custom middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write) manipulate the `HttpRequest` object into a state specified by its configuration until it reaches a request builder middleware, where it creates a `HttpRequestMessage` object, which is used to make a request to a downstream service.\r\nThe middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware.\r\nThe response from the downstream service is retrieved as the request goes back up the Ocelot pipeline.\r\nThere is a piece of middleware that maps the `HttpResponseMessage` onto the `HttpResponse` object, and that is returned to the client.\r\nThat is basically it, with a bunch of other features!\r\n\r\n## Install\r\nOcelot is designed to work with [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/) and it targets `net9.0` [STS](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types) and `net8.0`, `net10.0` [LTS](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types) target framework monikers ([TFMs](https://learn.microsoft.com/en-us/dotnet/standard/frameworks#supported-target-frameworks)). [^1]\r\n\r\nInstall [Ocelot](https://www.nuget.org/packages/Ocelot) package and its dependencies using NuGet package manager:\r\n```powershell\r\nInstall-Package Ocelot\r\n```\r\nOr via the .NET CLI:\r\n```shell\r\ndotnet add package Ocelot\r\n```\r\n> All versions are available [on NuGet](https://www.nuget.org/packages/Ocelot#versions-body-tab).\r\n\r\n## Documentation\r\n- [RST-sources](https://github.com/ThreeMammals/Ocelot/tree/develop/docs):\r\n  This includes the source code for the documentation (in reStructuredText format, .rst files), which is up to date for the current [development](https://github.com/ThreeMammals/Ocelot/tree/develop/).\r\n  And the rendered HTML documentation is available [here](https://ocelot.readthedocs.io/en/develop/).\r\n- [Read the Docs](https://ocelot.readthedocs.io):\r\n  This official website, in HTML format, contains a wealth of information and will be helpful if you want to understand the [features](#features) that Ocelot currently offers.\r\n  The rendered HTML documentation, which is currently in [development](https://github.com/ThreeMammals/Ocelot/tree/develop/docs), is available [here](https://ocelot.readthedocs.io/en/develop/).\r\n- [Ask Ocelot Guru](https://gurubase.io/g/ocelot):\r\n  It is an AI focused on Ocelot, designed to answer your questions. [^2]\r\n\r\n## Features\r\nThe primary features—[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) and [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html)—are always utilized by users, even in a minimal app setup, without customizations or extra configurations.\r\nOcelot's capabilities are categorized into three main groups of features: *solid*, *hybrid*, and *feature-family* groups, which are explained below.\r\n- *Solid features* are unique to Ocelot. They do not contain subfeatures and are not related to other features.\r\n- *Hybrid features*, on the other hand, have multiple relationships with other features and can be part of other features.\r\n- *Feature families* are large groups that consist of multiple subfeatures.\r\n\r\n| Group | Features |\r\n|-------|----------|\r\n|Primary|[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html)|\r\n| Solid |[Caching](https://ocelot.readthedocs.io/en/latest/features/caching.html), [Delegating Handlers](https://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html), [Quality of Service](https://ocelot.readthedocs.io/en/latest/features/qualityofservice.html)[^3], [Rate Limiting](https://ocelot.readthedocs.io/en/latest/features/ratelimiting.html)|\r\n| Hybrid|[Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html), [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html)[^4], [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Dependency Injection](https://ocelot.readthedocs.io/en/latest/features/dependencyinjection.html), [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html)|\r\n|Family|[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html), [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html), [Transformations](https://ocelot.readthedocs.io/en/latest/search.html?q=Transformation), [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html)[^5] |\r\n\r\nFeature groups are explained in the table below\r\n\r\n| Feature | Relationships and Notes |\r\n|---------|-------------------------|\r\n| [Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html) | [Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html) heavily depends on [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), and [Administration API](https://ocelot.readthedocs.io/en/latest/features/administration.html#administration-api) methods are part of [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), [Caching](https://ocelot.readthedocs.io/en/latest/features/caching.html), and [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) |\r\n| [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html)[^4] | [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html) relies on [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) |\r\n| [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html) | [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html) followed by [Authorization](https://ocelot.readthedocs.io/en/latest/features/authorization.html) |\r\n| [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) | [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) depends on [Dependency Injection](https://ocelot.readthedocs.io/en/latest/features/dependencyinjection.html), including `GET`/`POST` operations via the [Administration REST API](https://ocelot.readthedocs.io/en/latest/features/administration.html#administration-api), a specialized [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) scheme/protocol, advanced [Middleware Injection](https://ocelot.readthedocs.io/en/latest/features/middlewareinjection.html), and [Metadata](https://ocelot.readthedocs.io/en/latest/features/metadata.html)-based extensions |\r\n| [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) | [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) offers specialized [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) and [Dynamic Routing](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing) modes but does not support [GraphQL](https://ocelot.readthedocs.io/en/latest/features/graphql.html)[^6] |\r\n| [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html) | [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html) is a critical dependency for [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html) |\r\n| [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) | [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) includes [Error Handling](https://ocelot.readthedocs.io/en/latest/features/errorcodes.html) and [Tracing](https://ocelot.readthedocs.io/en/latest/features/tracing.html) |\r\n| [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html)[^5] | [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html) with the following discovery providers: [Consul](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#consul), [Kubernetes](https://ocelot.readthedocs.io/en/latest/features/kubernetes.html), [Eureka](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#eureka), and [Service Fabric](https://ocelot.readthedocs.io/en/latest/features/servicefabric.html) |\r\n| [Transformations](https://ocelot.readthedocs.io/en/latest/search.html?q=Transformation) | They provide transformations for [Claims](https://ocelot.readthedocs.io/en/latest/features/claimstransformation.html), [Headers](https://ocelot.readthedocs.io/en/latest/features/headerstransformation.html), and [Method](https://ocelot.readthedocs.io/en/latest/features/methodtransformation.html) |\r\n\r\n> Ocelot customizations can be configured using [Metadata](https://ocelot.readthedocs.io/en/latest/features/metadata.html), developed with [Delegating Handlers](https://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html), and in advanced scenarios, they can be developed and then configured with [Middleware Injection](https://ocelot.readthedocs.io/en/latest/features/middlewareinjection.html).\r\nFor further details, refer to the [Documentation](#documentation).\r\n\r\n## Contributing\r\nYou can see what we are working on in the [backlog](https://github.com/ThreeMammals/Ocelot/issues).\r\nWe love to receive contributions from the community, so please keep them coming.\r\nPull requests, issues, and commentary welcome! <img src=\"https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat.png\" alt=\"octocat\" height=\"25\" />\r\n\r\nPlease complete the relevant [template](https://github.com/ThreeMammals/Ocelot/tree/main/.github) for [issues](https://github.com/ThreeMammals/Ocelot/blob/main/.github/ISSUE_TEMPLATE.md) and [pull requests](https://github.com/ThreeMammals/Ocelot/blob/main/.github/PULL_REQUEST_TEMPLATE.md).\r\nSometimes it's worth getting in touch with us to [discuss](https://github.com/ThreeMammals/Ocelot/discussions) changes before doing any work in case this is something we are already doing or it might not make sense.\r\nWe can also give advice on the easiest way to do things <img src=\"https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat.png\" alt=\"octocat\" height=\"25\" />\r\n\r\nFinally, we mark all existing issues as [![label: help wanted][~helpwanted]](https://github.com/ThreeMammals/Ocelot/labels/help%20wanted)\r\n[![label: small effort][~smalleffort]](https://github.com/ThreeMammals/Ocelot/labels/small%20effort)\r\n[![label: medium effort][~mediumeffort]](https://github.com/ThreeMammals/Ocelot/labels/medium%20effort)\r\n[![label: large effort][~largeeffort]](https://github.com/ThreeMammals/Ocelot/labels/large%20effort).[^7]\r\nIf you want to contribute for the first time, we suggest looking at a [![label: help wanted][~helpwanted]](https://github.com/ThreeMammals/Ocelot/labels/help%20wanted) \r\n[![label: small effort][~smalleffort]](https://github.com/ThreeMammals/Ocelot/labels/small%20effort) \r\n[![label: good first issue][~goodfirstissue]](https://github.com/ThreeMammals/Ocelot/labels/good%20first%20issue) <img src=\"https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat.png\" alt=\"octocat\" height=\"25\" />\r\n\r\n[~helpwanted]: https://img.shields.io/badge/-help%20wanted-128A0C.svg\r\n[~smalleffort]: https://img.shields.io/badge/-small%20effort-fef2c0.svg\r\n[~mediumeffort]: https://img.shields.io/badge/-medium%20effort-e0f42c.svg\r\n[~largeeffort]: https://img.shields.io/badge/-large%20effort-10526b.svg\r\n[~goodfirstissue]: https://img.shields.io/badge/-good%20first%20issue-ffc4d8.svg\r\n\r\n### Notes\r\n[^1]: Starting with version [21](https://github.com/ThreeMammals/Ocelot/releases/tag/21.0.0) and higher, the solution's code base supports [Multitargeting](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-multitargeting-overview) as SDK-style projects. It should be easier for teams to migrate to the currently supported [.NET 8, 9 and 10](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#lifecycle) frameworks. Also, new features will be available for all .NET SDKs that we support via multitargeting. Find out more here: [Target frameworks in SDK-style projects](https://learn.microsoft.com/en-us/dotnet/standard/frameworks)\r\n[^2]: [Ocelot Guru](https://gurubase.io/g/ocelot) is an unofficial tool to get answers regarding Ocelot: please consider it an advanced search tool. Thus, we have an official [Questions & Answers](https://github.com/ThreeMammals/Ocelot/discussions/categories/q-a) category in the [Discussions](https://github.com/ThreeMammals/Ocelot/discussions) space.\r\n[^3]: Retry policies only via [Polly](https://github.com//App-vNext/Polly) library referenced within the [Ocelot.Provider.Polly](https://www.nuget.org/packages/Ocelot.Provider.Polly) extension package.\r\n[^4]: Previously, the [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html) feature was called [Request Aggregation](https://ocelot.readthedocs.io/en/23.4.3/features/requestaggregation.html) in versions [23.4.3](https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.3) and earlier. Internally, within the Ocelot team, this feature is referred to as [Multiplexer](https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/Multiplexer).\r\n[^5]: Ocelot supports the following service discovery providers: (**1**) [Consul](https://www.consul.io) through the [Ocelot.Provider.Consul](https://www.nuget.org/packages/Ocelot.Provider.Consul) extension package, (**2**) [Kubernetes](https://kubernetes.io) via the [Ocelot.Provider.Kubernetes](https://www.nuget.org/packages/Ocelot.Provider.Kubernetes) extension package, and (**3**) [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix), which utilizes the [Steeltoe.Discovery.Eureka](https://www.nuget.org/packages/Steeltoe.Discovery.Eureka) package referenced within the [Ocelot.Provider.Eureka](https://www.nuget.org/packages/Ocelot.Provider.Eureka) extension package. Additionally, Ocelot supports (**4**) Azure [Service Fabric](https://azure.microsoft.com/en-us/products/service-fabric/) for service discovery, along with special modes such as [Dynamic Routing](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing) and [Custom Providers](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#custom-providers).\r\n[^6]: Ocelot does not directly support [GraphQL](https://graphql.org/). Developers can easily integrate the [GraphQL for .NET](https://github.com/graphql-dotnet/graphql-dotnet) library. \r\n[^7]: See all [labels](https://github.com/ThreeMammals/Ocelot/issues/labels) for the repository, which are useful for searching and filtering.\r\n\r\n"
  },
  {
    "path": "ReleaseNotes.md",
    "content": "<!--\nTag to substitute: {0}\nhttps://www.nuget.org/packages/Ocelot/{0}\n-->\n## Pre-release 2 for [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) SDK (version [{0}](https://www.nuget.org/packages/Ocelot/{0}))\n> Milestone: [.NET 10](https://github.com/ThreeMammals/Ocelot/milestone/13)\n\nThis is **Pre-release 2** for the [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) SDK.\n\nVersion [{0}](https://www.nuget.org/packages/Ocelot/{0}) includes upgraded solutions and [NuGet packages](https://www.nuget.org/profiles/ThreeMammals) based on .NET SDK [10.0.201](https://dotnet.microsoft.com/en-us/download/dotnet/10.0), released on March 12, 2026.\nFor more details about SDK [10.0.201](https://dotnet.microsoft.com/en-us/download/dotnet/10.0), see the [Release notes](https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.5/10.0.5.md).\n\nDevelopment teams can start migrating their Ocelot-based projects using this [Beta 2](https://www.nuget.org/packages/Ocelot/{0}) release to upgrade to the .NET 10 SDK with Long-Term Support (LTS).\n"
  },
  {
    "path": "build.cake",
    "content": "﻿#tool dotnet:?package=GitVersion.Tool&version=6.6.2\r\n#tool nuget:?package=ReportGenerator&version=5.5.4\r\n\r\n#addin nuget:?package=Cake.Http\r\n#addin nuget:?package=Newtonsoft.Json&version=13.0.4 // Switch to a MS lib!\r\n#addin nuget:?package=System.Text.Encodings.Web&version=10.0.5\r\n\r\n#r \"Spectre.Console\"\r\nusing Spectre.Console;\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing _File_ = System.IO.File;\r\nusing _Directory_ = System.IO.Directory;\r\n\r\nbool IsTechnicalRelease = false;\r\nconst string Release = \"Release\"; // task name, target, and Release config name\r\nconst string PullRequest = \"PullRequest\"; // task name, target, and PullRequest config name\r\nconst string LatestFramework = \"LatestFramework\"; // task name, target, and LatestFramework config name\r\nconst string AllTFMs = \"net8.0;net9.0;net10.0\";\r\nconst string LatestTFM = \"net10.0\";\r\nstring NL = Environment.NewLine;\r\n\r\n// Create a CultureInfo object for UK English\r\nCultureInfo ukCulture = new(\"en-GB\");\r\nCultureInfo.DefaultThreadCurrentCulture = ukCulture;\r\nCultureInfo.DefaultThreadCurrentUICulture = ukCulture;\r\nInformation(\"Current Culture: \" + CultureInfo.CurrentCulture);\r\nInformation(\"Current UI Culture: \" + CultureInfo.CurrentUICulture);\r\n\r\n// Display culture properties\r\nInformation(\"Culture Name: \" + ukCulture.Name);              // en-GB\r\nInformation(\"Display Name: \" + ukCulture.DisplayName);       // English (United Kingdom)\r\nInformation(\"English Name: \" + ukCulture.EnglishName);       // English (United Kingdom)\r\nInformation(\"Native Name: \" + ukCulture.NativeName);         // English (United Kingdom)\r\nInformation(\"Two-letter ISO Language Name: \" + ukCulture.TwoLetterISOLanguageName); // en\r\nInformation(\"Three-letter ISO Language Name: \" + ukCulture.ThreeLetterISOLanguageName); // eng\r\nInformation(\"Region ISO Code: \" + new RegionInfo(ukCulture.Name).TwoLetterISORegionName); // GB\r\n\r\n// Example: format a date and currency in UK style\r\nDateTime now = DateTime.Now;\r\ndecimal amount = 12345.67m;\r\nInformation(\"Date (UK format): \" + now.ToString(\"D\", ukCulture));\r\nInformation(\"Currency (UK format): \" + amount.ToString(\"C\", ukCulture));\r\n\r\nvar compileConfig = Argument(\"configuration\", Release); // compile\r\nvar artifactsDir = Directory(\"artifacts\"); // build artifacts\r\n\r\n// unit testing\r\nvar artifactsForUnitTestsDir = artifactsDir + Directory(\"UnitTests\");\r\nvar unitTestAssemblies = @\"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\";\r\n\r\n// acceptance testing\r\nvar artifactsForAcceptanceTestsDir = artifactsDir + Directory(\"AcceptanceTests\");\r\nvar acceptanceTestAssemblies = @\"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\";\r\n\r\n// benchmark testing\r\nvar artifactsForBenchmarkTestsDir = artifactsDir + Directory(\"BenchmarkTests\");\r\nvar benchmarkTestAssemblies = @\"./test/Ocelot.Benchmarks\";\r\n\r\n// packaging\r\nvar packagesDir = artifactsDir + Directory(\"Packages\");\r\nvar artifactsFile = packagesDir + File(\"artifacts.txt\");\r\nvar releaseNotesFile = packagesDir + File(\"ReleaseNotes.md\");\r\nvar releaseNotes = new List<string>();\r\n\r\n// internal build variables - don't change these.\r\nstring committedVersion = \"0.0.0-dev\";\r\nGitVersion versioning = null;\r\n\r\nvar target = Argument(\"target\", \"Default\");\r\nvar slnFile = \"./Ocelot.slnx\";\r\n\r\nInformation($\"{NL}Target: {target}\");\r\nInformation($\"Build: {compileConfig}\");\r\nInformation($\"Solution: {slnFile}\");\r\n\r\nTaskTeardown(context => {\r\n\tAnsiConsole.Markup($\"[green]DONE[/] {context.Task.Name}\" + NL);\r\n});\r\n\r\nTask(\"Default\")\r\n\t.IsDependentOn(\"Build\");\r\nTask(\"Build\")\r\n\t.IsDependentOn(\"Tests\");\r\nTask(\"LatestFramework\")\r\n\t.IsDependentOn(\"Tests\");\r\nTask(\"PullRequest\")\r\n\t.IsDependentOn(\"Tests\");\r\n\r\nTask(\"ReleaseNotes\")\r\n\t.IsDependentOn(\"CreateReleaseNotes\");\r\n\r\nTask(\"Tests\")\r\n\t.IsDependentOn(\"UnitTests\")\r\n\t.IsDependentOn(\"AcceptanceTests\");\r\n\r\nTask(\"Release\")\r\n\t.IsDependentOn(\"Build\")\r\n\t.IsDependentOn(\"CreateReleaseNotes\")\r\n\t.IsDependentOn(\"CreateArtifacts\")\r\n\t.IsDependentOn(\"PublishGitHubRelease\")\r\n    .IsDependentOn(\"PublishToNuget\");\r\n\r\nTask(\"Restore\")\r\n    .Does(() =>\r\n\t{\r\n\t\tvar settings = new DotNetRestoreSettings\r\n\t\t{\r\n\t\t\tLockedMode = true, // equivalent to --locked-mode\r\n\t\t\t// UseLockFile = true, // equivalent to --use-lock-file\r\n\t\t\t// Sources = new[] { \"https://api.nuget.org/v3/index.json\" }\r\n\t\t};\r\n\t\tDotNetRestore(slnFile, settings);\r\n\t});\r\n\r\nTask(\"Compile\")\r\n\t.IsDependentOn(\"Clean\")\r\n\t.IsDependentOn(\"Version\")\r\n\t.IsDependentOn(\"Restore\")\r\n\t.Does(() =>\r\n\t{\t\r\n\t\tPreprocessReadMe();\r\n\t\tInformation(\"Branch: \" + GetBranchName());\r\n\t\tInformation(\"Build: \" + compileConfig);\r\n\t\tInformation(\"Solution: \" + slnFile);\r\n\t\tvar settings = new DotNetBuildSettings\r\n\t\t{\r\n\t\t\tConfiguration = compileConfig,\r\n\t\t\tNoRestore = true,\r\n\t\t};\r\n\t\tif (target == LatestFramework || target == PullRequest)\r\n\t\t{\r\n\t\t\tsettings.Framework = LatestTFM; // build using .NET 10 SDK only\r\n\t\t}\r\n\t\tstring frameworkInfo = string.IsNullOrEmpty(settings.Framework) ? AllTFMs : settings.Framework;\r\n\t\tInformation($\"Settings {nameof(DotNetBuildSettings.Framework)}: {frameworkInfo}\");\r\n\t\tInformation($\"Settings {nameof(DotNetBuildSettings.Configuration)}: {settings.Configuration}\");\r\n\t\tDotNetBuild(slnFile, settings);\r\n\t});\r\n\r\nTask(\"Clean\")\r\n\t.Does(() =>\r\n\t{\r\n        if (DirectoryExists(artifactsDir))\r\n        {\r\n            DeleteDirectory(artifactsDir, new DeleteDirectorySettings {\r\n\t\t\t\tRecursive = true,\r\n\t\t\t\tForce = true\r\n\t\t\t});\r\n        }\r\n        CreateDirectory(artifactsDir);\r\n\t});\r\n\r\nTask(\"Version\")\r\n\t.Does(() =>\r\n\t{\r\n\t\tversioning = GetNuGetVersionForCommit();\r\n\t\tversioning.NuGetVersion ??= versioning.SemVer;\r\n\t\tif (target == Release && IsRunningInCICD() && IsMainBranch() && versioning.SemVer.Contains(\"-\")) // dash -> suffix in version\r\n\t\t\tversioning.NuGetVersion = versioning.MajorMinorPatch; // when releasing from main branch the tag should not contain suffix after dash char\r\n\r\n\t\tInformation(\"#########################\");\r\n\t\tInformation(\"# SemVer Information\");\r\n\t\tInformation(\"#========================\");\r\n\t\tInformation($\"# {nameof(versioning.NuGetVersion)}: {versioning.NuGetVersion}\");\r\n\t\tInformation($\"# {nameof(versioning.BranchName)}: {versioning.BranchName}\");\r\n\t\tInformation($\"# {nameof(versioning.MajorMinorPatch)}: {versioning.MajorMinorPatch}\");\r\n\t\tInformation($\"# {nameof(versioning.SemVer)}: {versioning.SemVer}\");\r\n\t\tInformation($\"# {nameof(versioning.InformationalVersion)}: {versioning.InformationalVersion}\");\r\n\t\tInformation(\"#########################\");\r\n\r\n\t\tInformation($\"Persisting version number... {nameof(versioning.NuGetVersion)} -> {versioning.NuGetVersion}\");\r\n\t\tPersistVersion(committedVersion, versioning.NuGetVersion);\r\n\t});\r\n\r\nTask(\"GitLogUniqContributors\")\r\n\t.Does(() =>\r\n\t{\r\n\t\tvar command = \"log --format=\\\"%aN|%aE\\\" \";\r\n\t\t// command += IsRunningInCICD() ? \"| sort | uniq\" :\r\n\t\t// \tIsRunningInPowershell() ? \"| Sort-Object -Unique\" : \"| sort | uniq\";\r\n\t\tList<string> output = GitHelper(command);\r\n\t\toutput.Sort();\r\n\t\tList<string> contributors = output.Distinct().ToList();\r\n\t\tcontributors.Sort();\r\n        Information($\"Detected {contributors.Count} unique contributors:\");\r\n        Information(string.Join(NL, contributors));\r\n\t\t// TODO Search example in bash: curl -L -H \"X-GitHub-Api-Version: 2022-11-28\"   \"https://api.github.com/search/users?q=Chris+Swinchatt\"\r\n        Information(NL + \"Unicode test: 1) Raynald Messié; 2) 彭伟 pengweiqhca\");\r\n        AnsiConsole.Markup(\"Unicode test: 1) Raynald Messié; 2) 彭伟 pengweiqhca\" + NL);\r\n\t\t// Powershell life hack: $OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding\r\n\t\t// https://stackoverflow.com/questions/40098771/changing-powershells-default-output-encoding-to-utf-8\r\n\t\t// https://stackoverflow.com/questions/49476326/displaying-unicode-in-powershell/49481797#49481797\r\n\t\t// https://stackoverflow.com/questions/57131654/using-utf-8-encoding-chcp-65001-in-command-prompt-windows-powershell-window/57134096#57134096\r\n\t});\r\n\r\nTask(\"CreateReleaseNotes\")\r\n\t.IsDependentOn(\"Version\")\r\n\t//.IsDependentOn(\"GitLogUniqContributors\")\r\n\t.Does(() =>\r\n\t{\r\n        Information($\"Generating release notes at {releaseNotesFile}\");\r\n        var lastReleaseTags = GitHelper(\"describe --tags --abbrev=0 --exclude net*\");\r\n        var lastRelease = lastReleaseTags.First(t => !t.StartsWith(\"net\")); // skip 'net*-vX.Y.Z' tag and take 'major.minor.build'\r\n        var releaseVersion = versioning.NuGetVersion;\r\n\r\n        // Read main header from Git file, substitute version in header, and add content further...\r\n        Information(\"{0}  New release tag is \" + releaseVersion);\r\n        Information(\"{1} Last release tag is \" + lastRelease);\r\n        var body = _File_.ReadAllText(\"./ReleaseNotes.md\", System.Text.Encoding.UTF8);\r\n        var releaseHeader = string.Format(body, releaseVersion, lastRelease);\r\n        releaseNotes = new List<string> { releaseHeader };\r\n        if (IsTechnicalRelease)\r\n        {\r\n            WriteReleaseNotes();\r\n            return;\r\n        }\r\n\r\n        const bool debugUserEmail = false;\r\n        var shortlogSummary = GitHelper($\"shortlog --no-merges --numbered --summary --email {lastRelease}..HEAD\")\r\n            .ToList();\r\n        var re = new Regex(@\"^[\\s\\t]*(?'commits'\\d+)[\\s\\t]+(?'author'.*)[\\s\\t]+<(?'email'.*)>.*$\");\r\n        static SummaryItem CreateSummaryItem(System.Text.RegularExpressions.Match m) => new()\r\n        {\r\n            Commits = int.Parse(m.Groups[\"commits\"]?.Value ?? \"0\"),\r\n            Author = m.Groups[\"author\"]?.Value?.Trim() ?? string.Empty,\r\n            Email = m.Groups[\"email\"]?.Value?.Trim() ?? string.Empty,\r\n        };\r\n        var summary = shortlogSummary\r\n            .Where(x => re.IsMatch(x))\r\n            .Select(x => re.Match(x))\r\n            .Select(CreateSummaryItem)\r\n            .ToList();\r\n\r\n        // Starring aka Release Influencers\r\n        var starring = new List<string>();\r\n        string CreateStars(int count, string name)\r\n        {\r\n            var contributor = summary.Find(x => x.Author.Equals(name));\r\n            var stars = string.Join(string.Empty, Enumerable.Repeat(\":star:\", count));\r\n            var emailInfo = debugUserEmail ? \", \" + contributor.Email : string.Empty;\r\n            return $\"{stars}  {contributor.Author}{emailInfo}\";\r\n        }\r\n\r\n        Information(\"------==< Old Starring >==------\");\r\n        foreach (var contributor in summary)\r\n        {\r\n            starring.Add(CreateStars(contributor.Commits, contributor.Author));\r\n        }\r\n        Information(string.Join(NL, starring));\r\n\r\n        var commitsGrouping = summary\r\n            .GroupBy(x => x.Commits)\r\n            .Select(CreateCommitsGroupingItem)\r\n            .OrderByDescending(x => x.Commits)\r\n            .ToList();\r\n        starring = IterateCommits(commitsGrouping,\r\n            breaker: log => false, // don't break, so iterate all groups (summary)\r\n            byCommits: (log, group) => CreateStars(group.Commits, group.Authors.First()),\r\n            byFiles: (log, group, fGroup) => CreateStars(group.Commits, fGroup.Contributors.First().Contributor),\r\n            byInsertions: (log, group, fGroup, insGroup) => CreateStars(group.Commits, insGroup.Contributors.First().Contributor),\r\n            byDeletions: (log, group, fGroup, insGroup, contributor) => CreateStars(group.Commits, contributor.Contributor));\r\n        Information(\"------==< New Starring >==------\");\r\n        Information(string.Join(NL, starring));\r\n\r\n        // Honoring aka Top Contributors\r\n        var coreTeamNames = new List<string> { \"Raman Maksimchuk\", \"Raynald Messié\", \"Guillaume Gnaegi\" }; // Ocelot Core team members should not be in Top 3 Chart\r\n        var coreTeamEmails = new List<string> { \"dotnet044@gmail.com\", \"redbird_project@yahoo.fr\", \"58469901+ggnaegi@users.noreply.github.com\" };\r\n        static CommitsGroupingItem CreateCommitsGroupingItem(IGrouping<int, SummaryItem> g) => new()\r\n        {\r\n            Commits = g.Key,\r\n            Count = g.Count(),\r\n            Authors = g.Select(x => x.Author).ToArray(),\r\n        };\r\n        commitsGrouping = summary\r\n            .Where(x => !coreTeamNames.Contains(x.Author) && !coreTeamEmails.Contains(x.Email)) // filter out Ocelot Core team members\r\n            .GroupBy(x => x.Commits)\r\n            .Select(CreateCommitsGroupingItem)\r\n            .OrderByDescending(x => x.Commits)\r\n            .ToList();\r\n        var topContributors = IterateCommits(commitsGrouping,\r\n            breaker: log => false, // (log.Count >= 3), // going to create Top 3\r\n            byCommits: (log, group) =>\r\n            {\r\n                var place = Place(log.Count);\r\n                var author = group.Authors.First();\r\n                return Honor(place, author, group.Commits);\r\n            },\r\n            byFiles: (log, group, fGroup) =>\r\n            {\r\n                var place = Place(log.Count);\r\n                var contributor = fGroup.Contributors.First();\r\n                return HonorForFiles(place, contributor.Contributor, group.Commits, contributor.Files);\r\n            },\r\n            byInsertions: (log, group, fGroup, insGroup) =>\r\n            {\r\n                var place = Place(log.Count);\r\n                var contributor = insGroup.Contributors.First();\r\n                return HonorForInsertions(place, contributor.Contributor, group.Commits, contributor.Files, contributor.Insertions);\r\n            },\r\n            byDeletions: (log, group, fGroup, insGroup, contributor) =>\r\n            {\r\n                var place = Place(log.Count);\r\n                return HonorForDeletions(place, contributor.Contributor, group.Commits, contributor.Files, contributor.Insertions, contributor.Deletions);\r\n            });\r\n        Information(\"------==< TOP Contributors >==------\");\r\n        Information(string.Join(NL, topContributors));\r\n\r\n        // local helpers\r\n        static string Place(int i) => ++i == 1 ? \"1st\" : i == 2 ? \"2nd\" : i == 3 ? \"3rd\" : $\"{i}th\";\r\n        static string Plural(int n) => n == 1 ? \"\" : \"s\";\r\n        static string Honor(string place, string author, int commits, string suffix = null)\r\n            => $\"{place[0]}<sup>{place[1..]}</sup> :{place}_place_medal: goes to **{author}** for delivering **{commits}** feature{Plural(commits)} {suffix ?? \"\"}\";\r\n        static string HonorForFiles(string place, string author, int commits, int files, string suffix = null)\r\n            => Honor(place, author, commits, $\"in **{files}** file{Plural(files)} changed {suffix ?? \"\"}\");\r\n        static string HonorForInsertions(string place, string author, int commits, int files, int insertions, string suffix = null)\r\n            => HonorForFiles(place, author, commits, files, $\"with **{insertions}** insertion{Plural(insertions)} {suffix ?? \"\"}\");\r\n        static string HonorForDeletions(string place, string author, int commits, int files, int insertions, int deletions)\r\n            => HonorForInsertions(place, author, commits, files, insertions, $\"and **{deletions}** deletion{Plural(deletions)}\");\r\n        List<string> IterateCommits(List<CommitsGroupingItem> commitsGrouping, Predicate<List<string>> breaker,\r\n            Func<List<string>, CommitsGroupingItem, string> byCommits,\r\n            Func<List<string>, CommitsGroupingItem, FilesGroupingItem, string> byFiles,\r\n            Func<List<string>, CommitsGroupingItem, FilesGroupingItem, InsertionsGroupingItem, string> byInsertions,\r\n            Func<List<string>, CommitsGroupingItem, FilesGroupingItem, InsertionsGroupingItem, FilesChangedItem, string> byDeletions)\r\n        {\r\n            var log = new List<string>();\r\n            foreach (var group in commitsGrouping)\r\n            {\r\n                if (breaker.Invoke(log)) break; // (log.Count >= top3)\r\n                if (group.Count == 1)\r\n                {\r\n                    log.Add(byCommits.Invoke(log, group));\r\n                }\r\n                else // multiple candidates with the same number of commits, so, group by files changed\r\n                {\r\n                    var statistics = new List<FilesChangedItem>();\r\n                    var shortstatRegex = new Regex(@\"^\\s*(?'files'\\d+)\\s+files?\\s+changed(?'ins',\\s+(?'insertions'\\d+)\\s+insertions?\\(\\+\\))?(?'del',\\s+(?'deletions'\\d+)\\s+deletions?\\(\\-\\))?\\s*$\");\r\n                    static FilesChangedItem CreateFilesChangedItem(System.Text.RegularExpressions.Match m)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tFilesChangedItem item = new();\r\n\t\t\t\t\t\tif (int.TryParse(m.Groups[\"files\"]?.Value ?? \"0\", out int files))\r\n            \t\t\t\titem.Files = files;\r\n\t\t\t\t\t\telse\r\n            \t\t\t\titem.Files = 0;\r\n\r\n\t\t\t\t\t\tif (int.TryParse(m.Groups[\"insertions\"]?.Value ?? \"0\", out int insertions))\r\n            \t\t\t\titem.Insertions = insertions;\r\n\t\t\t\t\t\telse\r\n            \t\t\t\titem.Insertions = 0;\r\n\r\n\t\t\t\t\t\tif (int.TryParse(m.Groups[\"deletions\"]?.Value ?? \"0\", out int deletions))\r\n            \t\t\t\titem.Deletions = deletions;\r\n\t\t\t\t\t\telse\r\n            \t\t\t\titem.Deletions = 0;\r\n\t\t\t\t\t\treturn item;\r\n\t\t\t\t\t}\r\n                    foreach (var author in group.Authors) // Collect statistics from git log & shortlog\r\n                    {\r\n                        if (!statistics.Exists(s => s.Contributor == author))\r\n                        {\r\n                            var shortstat = GitHelper($\"log --no-merges --author=\\\"{author}\\\" --shortstat --pretty=oneline {lastRelease}..HEAD\");\r\n                            var data = shortstat\r\n                                .Where(x => shortstatRegex.IsMatch(x))\r\n                                .Select(x => shortstatRegex.Match(x))\r\n                                .Select(CreateFilesChangedItem)\r\n                                .ToList();\r\n                            statistics.Add(new FilesChangedItem(author, data.Sum(x => x.Files), data.Sum(x => x.Insertions), data.Sum(x => x.Deletions)));\r\n                        }\r\n                    }\r\n                    var filesGrouping = statistics\r\n                        .GroupBy(x => x.Files)\r\n                        .Select(g => new FilesGroupingItem\r\n                        {\r\n                            Files = g.Key,\r\n                            Count = g.Count(),\r\n                            Contributors = g.SelectMany(x => statistics.Where(s => s.Contributor == x.Contributor && s.Files == g.Key)).ToArray(),\r\n                        })\r\n                        .OrderByDescending(x => x.Files)\r\n                        .ToList();\r\n                    foreach (var fGroup in filesGrouping)\r\n                    {\r\n                        if (breaker.Invoke(log)) break;\r\n                        if (fGroup.Count == 1)\r\n                        {\r\n                            log.Add(byFiles.Invoke(log, group, fGroup));\r\n                        }\r\n                        else // multiple candidates with the same number of commits, with the same number of changed files, so, group by additions (insertions)\r\n                        {\r\n                            var insertionsGrouping = fGroup.Contributors\r\n                                .GroupBy(x => x.Insertions)\r\n                                .Select(g => new InsertionsGroupingItem\r\n                                {\r\n                                    Insertions = g.Key,\r\n                                    Count = g.Count(),\r\n                                    Contributors = g.SelectMany(x => fGroup.Contributors.Where(s => s.Contributor == x.Contributor && s.Insertions == g.Key)).ToArray(),\r\n                                })\r\n                                .OrderByDescending(x => x.Insertions)\r\n                                .ToList();\r\n                            foreach (var insGroup in insertionsGrouping)\r\n                            {\r\n                                if (breaker.Invoke(log)) break;\r\n                                if (insGroup.Count == 1)\r\n                                {\r\n                                    log.Add(byInsertions.Invoke(log, group, fGroup, insGroup));\r\n                                }\r\n                                else // multiple candidates with the same number of commits, with the same number of changed files, with the same number of insertions, so, order desc by deletions\r\n                                {\r\n                                    foreach (var contributor in insGroup.Contributors.OrderByDescending(x => x.Deletions))\r\n                                    {\r\n                                        if (breaker.Invoke(log)) break;\r\n                                        log.Add(byDeletions.Invoke(log, group, fGroup, insGroup, contributor));\r\n                                    }\r\n                                }\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            return log;\r\n        } // END of IterateCommits\r\n        // releaseNotes.Add(\"### Honoring :medal_sports: aka Top Contributors :clap:\");\r\n        // releaseNotes.AddRange(topContributors.Take(3)); // Top 3 only, disabled 'breaker' logic\r\n        // releaseNotes.Add(\"\");\r\n        // releaseNotes.Add(\"### Starring :star: aka Release Influencers :bowtie:\");\r\n        // releaseNotes.AddRange(starring);\r\n        // releaseNotes.Add(\"\");\r\n        // releaseNotes.Add($\"### Features in Release {releaseVersion}\");\r\n        // releaseNotes.Add(\"\");\r\n        // releaseNotes.Add(\"<details><summary>Logbook</summary>\");\r\n        // releaseNotes.Add(\"\");\r\n        // var commitsHistory = GitHelper($\"log --no-merges --date=format:\\\"%A, %B %d at %H:%M\\\" --pretty=format:\\\"- <sub>%h by **%aN** on %ad &rarr;</sub>%n  %s\\\" {lastRelease}..HEAD\");\r\n        // releaseNotes.AddRange(commitsHistory);\r\n        // releaseNotes.Add(\"</details>\");\r\n        // releaseNotes.Add(\"\");\r\n        WriteReleaseNotes();\r\n\t});\r\n\r\nstruct SummaryItem\r\n{\r\n\tpublic int Commits;\r\n\tpublic string Author;\r\n\tpublic string Email;\r\n}\r\nstruct CommitsGroupingItem\r\n{\r\n\tpublic int Commits;\r\n\tpublic int Count;\r\n\tpublic string[] Authors;\r\n}\r\nstruct FilesChangedItem\r\n{\r\n\tpublic string Contributor;\r\n\tpublic int Files;\r\n\tpublic int Insertions;\r\n\tpublic int Deletions;\r\n\tpublic FilesChangedItem(string author, int files, int insertions, int deletions)\r\n\t{\r\n\t\tContributor = author;\r\n\t\tFiles = files;\r\n\t\tInsertions = insertions;\r\n\t\tDeletions = deletions;\r\n\t}\r\n}\r\nstruct FilesGroupingItem\r\n{\r\n\tpublic int Files;\r\n\tpublic int Count;\r\n\tpublic FilesChangedItem[] Contributors;\r\n}\r\nstruct InsertionsGroupingItem\r\n{\r\n\tpublic int Insertions;\r\n\tpublic int Count;\r\n\tpublic FilesChangedItem[] Contributors;\r\n}\r\n\r\nprivate List<string> GitHelper(string command)\r\n{\r\n\tIEnumerable<string> output;\r\n\tvar exitCode = StartProcess(\r\n\t\t\"git\",\r\n\t\tnew ProcessSettings { Arguments = command, RedirectStandardOutput = true },\r\n\t\tout output);\r\n\tif (exitCode != 0)\r\n\t\tthrow new Exception(\"Failed to execute Git command: \" + command);\r\n\treturn output.ToList();\r\n}\r\n\r\nprivate void WriteReleaseNotes()\r\n{\r\n\tInformation($\"RUN {nameof(WriteReleaseNotes)} ...\");\r\n\tEnsureDirectoryExists(packagesDir);\r\n\t_File_.WriteAllLines(releaseNotesFile, releaseNotes, Encoding.UTF8);\r\n\tvar content = _File_.ReadAllText(releaseNotesFile, Encoding.UTF8);\r\n\tif (string.IsNullOrEmpty(content))\r\n\t{\r\n\t\t_File_.WriteAllText(releaseNotesFile, \"No commits since last release\", System.Text.Encoding.UTF8);\r\n\t}\r\n\tInformation(\"Release notes are >>>{0}<<<\", NL + content);\r\n}\r\n\r\nprivate List<string> GetTFMs()\r\n{\r\n\tvar tfms = AllTFMs.Split(';').ToList();\r\n\tif (target == LatestFramework || target == \"UnitTests\" || target == Release || target == PullRequest)\r\n    {\r\n        tfms.Clear();\r\n        tfms.Add(LatestTFM);\r\n    }\r\n\treturn tfms;\r\n}\r\nTask(\"UnitTests\")\r\n\t.IsDependentOn(\"Compile\")\r\n\t.Does(() =>\r\n\t{\r\n\t\tvar verbosity = IsRunningInCICD() ? \"minimal\" : \"normal\";\r\n\t\t// Sequential processing as an emulation of Visual Studio Test Explorer\r\n\t\tforeach (string tfm in GetTFMs())\r\n\t\t{\r\n\t\t\tvar settings = new DotNetTestSettings\r\n\t\t\t{\r\n\t\t\t\tConfiguration = compileConfig,\r\n\t\t\t\tResultsDirectory = artifactsForUnitTestsDir,\r\n\t\t\t\tArgumentCustomization = args => args\r\n\t\t\t\t\t.Append(\"--no-restore\")\r\n\t\t\t\t\t.Append(\"--no-build\")\r\n\t\t\t\t\t.Append(\"--collect:\\\"XPlat Code Coverage\\\"\") // this create the code coverage report\r\n\t\t\t\t\t.Append(\"--settings coverlet.runsettings\") // exclude Ocelot.Testing assembly from coverage\r\n\t\t\t\t\t.Append(\"--verbosity:\" + verbosity)\r\n\t\t\t\t\t.Append(\"--consoleLoggerParameters:ErrorsOnly\"),\r\n\t\t\t\tFramework = tfm,\r\n\t\t\t};\r\n\t\t\tInformation($\"Settings {nameof(settings.Framework)}: {settings.Framework}\");\r\n\t\t\tEnsureDirectoryExists(artifactsForUnitTestsDir);\r\n\t\t\tDotNetTest(unitTestAssemblies, settings); // sequential testing\r\n\t\t}\r\n\t\t\r\n\t\tInformation(\"ArtifactsForUnitTestsDir = \" + artifactsForUnitTestsDir);\r\n\t\tvar coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir)\r\n\t\t\t.First()\r\n\t\t\t.CombineWithFilePath(File(\"coverage.cobertura.xml\"));\r\n\t\tInformation(\"CoverageSummaryFile = \" + coverageSummaryFile);\r\n\t\tGenerateReport(coverageSummaryFile);\r\n\t\tInformation(\"##############################\");\r\n\t\tInformation(\"# Code coverage\");\r\n\t\tInformation(\"#=============================\");\r\n\r\n\t\t// TODO Implement reporting to the Action Run summary as an attachment or artifact\r\n\t\tconst string CoverallsRepo = \"https://coveralls.io/github/ThreeMammals/Ocelot\";\r\n\t\tInformation($\"# There is dedicated Coveralls step of GH Action workflows. So, we won't publish the coverage report to coveralls.io\");\r\n\r\n\t\t// Apply code coverage threshold\r\n\t\tconst double MinCodeCoverage = 0.93D; // consider definition of an env var in GitHub Environment vars\r\n\t\tvar lineCoverage = XmlPeek(coverageSummaryFile, \"//coverage/@line-rate\");\r\n\t\tvar branchCoverage = XmlPeek(coverageSummaryFile, \"//coverage/@branch-rate\");\r\n\t\tInformation(\"# Line Coverage: \" + lineCoverage);\r\n\t\tInformation(\"# Branch Coverage: \" + branchCoverage);\r\n\t\tif (double.Parse(lineCoverage) < MinCodeCoverage)\r\n\t\t{\r\n\t\t\tvar whereToCheck = !IsRunningInCICD() ? CoverallsRepo : artifactsForUnitTestsDir;\r\n\t\t\tvar msg = $\"# Code coverage fell below the threshold of {MinCodeCoverage * 100}%. You can find the code coverage report at {whereToCheck}\";\r\n\t\t\tWarning(msg);\r\n\t\t\t// throw new Exception(msg); // fail the building job step in GitHub Actions\r\n\t\t};\r\n\t\tInformation(\"##############################\");\r\n\t});\r\n\r\nTask(\"AcceptanceTests\")\r\n\t.IsDependentOn(\"Compile\")\r\n\t.Does(() =>\r\n\t{\r\n\t\tvar verbosity = IsRunningInCICD() ? \"minimal\" : \"normal\";\r\n\t\tif (IsRunningInCICD() && target == Release)\r\n\t\t{\r\n\t\t\tWarning(\"We are rolling out a release through the CI/CD pipeline, so we won't be running acceptance tests this time!\");\r\n\t\t\treturn;\r\n\t\t}\r\n        // Sequential processing as an emulation of Visual Studio Test Explorer\r\n\t\tforeach (string tfm in GetTFMs())\r\n\t\t{\r\n\t\t\tvar settings = new DotNetTestSettings\r\n\t\t\t{\r\n\t\t\t\tConfiguration = compileConfig,\r\n\t\t\t\tArgumentCustomization = args => args\r\n\t\t\t\t\t.Append(\"--no-restore\")\r\n\t\t\t\t\t.Append(\"--no-build\")\r\n\t\t\t\t\t.Append(\"--verbosity:\" + verbosity),\r\n\t\t\t\tFramework = tfm,\r\n\t\t\t};\r\n\t\t\tInformation($\"Settings {nameof(settings.Framework)}: {settings.Framework}\");\r\n\t\t\tEnsureDirectoryExists(artifactsForAcceptanceTestsDir);\r\n\t\t\tDotNetTest(acceptanceTestAssemblies, settings);\r\n\t\t}\r\n\t});\r\n\r\nTask(\"CreateArtifacts\")\r\n\t.IsDependentOn(\"CreateReleaseNotes\")\r\n\t.IsDependentOn(\"Compile\")\r\n\t.Does(() =>\r\n\t{\r\n\t\tWriteReleaseNotes();\r\n\t\t_File_.AppendAllLines(artifactsFile, new[] { \"ReleaseNotes.md\" });\r\n\r\n\t\tif (!IsTechnicalRelease)\r\n\t\t{\r\n\t\t\tCopyFiles(\"./**/Release/Ocelot.*.{nupkg,snupkg}\", packagesDir);\r\n\t\t\tvar projectFiles = GetFiles(\"./**/Release/Ocelot.*.{nupkg,snupkg}\")\r\n\t\t\t\t.OrderBy(f => f.GetFilenameWithoutExtension().ToString())\r\n\t\t\t\t.ThenBy(f => f.GetExtension().ToString()) // .nupkg first\r\n\t\t\t\t.ToList();\r\n\t\t\tforeach(var projectFile in projectFiles)\r\n\t\t\t{\r\n\t\t\t\t_File_.AppendAllLines(artifactsFile, new[] { projectFile.GetFilename().FullPath });\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar artifacts = _File_.ReadAllLines(artifactsFile)\r\n\t\t\t.Distinct();\r\n\r\n\t\tInformation($\"Listing all {nameof(artifacts)}...\");\r\n\t\tforeach (var artifact in artifacts)\r\n\t\t{\r\n\t\t\tvar codePackage = packagesDir + File(artifact);\r\n\t\t\tif (FileExists(codePackage))\r\n\t\t\t{\r\n\t\t\t\tInformation(\"Created package \" + codePackage);\r\n\t\t\t} else {\r\n\t\t\t\tInformation(\"Package does not exist: \" + codePackage);\r\n\t\t\t}\r\n\t\t}\r\n\t});\r\n\r\nTask(\"PublishGitHubRelease\")\r\n\t.IsDependentOn(\"CreateArtifacts\")\r\n\t.Does(() => \r\n\t{\r\n\t\tif (!IsRunningInCICD())\r\n\t\t{\r\n\t\t\tWarning(\"We are not running on the CI/CD so we won't publish a GitHub release\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tdynamic release = CreateGitHubRelease();\r\n\t\tvar path = packagesDir.ToString() + @\"/**/*Ocelot.*\"; // filter out artifacts.txt and ReleaseNotes.md\r\n\t\tvar files = GetFiles(path).ToList();\r\n\t\tforeach (var file in files)\r\n\t\t{\r\n\t\t\tUploadFileToGitHubRelease(release, file);\r\n\t\t}\r\n\t\tCompleteGitHubRelease(release);\r\n\t});\r\n\r\nTask(\"EnsureStableReleaseRequirements\")\r\n    .Does(() =>\t\r\n    {\r\n\t\tInformation(\"Check if stable release...\");\r\n\r\n        if (!IsRunningInCICD())\r\n\t\t{\r\n           throw new Exception(\"Stable release should happen via CI/CD\");\r\n\t\t}\r\n\r\n\t\tInformation(\"Release is stable...\");\r\n\t});\r\n\r\nTask(\"DownloadGitHubReleaseArtifacts\")\r\n    .Does(async () =>\r\n    {\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// hack to let GitHub catch up, todo - refactor to poll\r\n\t\t\tSystem.Threading.Thread.Sleep(5000);\r\n\t\t\tEnsureDirectoryExists(packagesDir);\r\n\r\n\t\t\tvar releaseUrl = \"https://api.github.com/repos/ThreeMammals/ocelot/releases/tags/\" + versioning.NuGetVersion;\r\n\t\t\tvar releaseInfo = await GetResourceAsync(releaseUrl);\r\n        \tvar assets_url = Newtonsoft.Json.Linq.JObject.Parse(releaseInfo)\r\n\t\t\t\t.Value<string>(\"assets_url\");\r\n\r\n\t\t\tvar assets = await GetResourceAsync(assets_url);\r\n\t\t\tforeach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JArray>(assets))\r\n\t\t\t{\r\n\t\t\t\tvar file = packagesDir + File(asset.Value<string>(\"name\"));\r\n\t\t\t\tDownloadFile(asset.Value<string>(\"browser_download_url\"), file);\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch(Exception exception)\r\n\t\t{\r\n\t\t\tInformation(\"There was an exception \" + exception);\r\n\t\t\tthrow;\r\n\t\t}\r\n\t});\r\n\r\nTask(\"PublishToNuget\")\r\n    .IsDependentOn(\"DownloadGitHubReleaseArtifacts\")\r\n    .Does(() =>\r\n    {\r\n\t\tif (IsTechnicalRelease)\r\n\t\t{\r\n\t\t\tInformation(\"Skipping of publishing to NuGet because of technical release...\");\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif (!IsRunningInCICD())\r\n\t\t{\r\n\t\t\tWarning(\"We are not running on the CI/CD so we won't publish NuGet packages.\");\r\n\t\t\t//return;\r\n\t\t}\r\n\t\tvar nugetFeedStableKey = EnvironmentVariable(\"OCELOT_NUGET_API_KEY_2025\");\r\n\t\tvar nugetFeedStableUploadUrl = \"https://www.nuget.org/api/v2/package\";\r\n\t\tvar nugetFeedStableSymbolsUploadUrl = \"https://www.nuget.org/api/v2/package\";\r\n\t\tPublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);\r\n\t});\r\n\r\nTask(\"Void\").Does(() => {});\r\n\r\nRunTarget(target);\r\n\r\nprivate void PreprocessReadMe()\r\n{\r\n\tconst string READMEmd = \"./README.md\";\r\n\tconst string RTD_NuGet_Valid_Domain = \"[ReadTheDocs][~docspassing]\";\r\n\tconst string RTD_Version_Latest  = \"[ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=latest&style=flat-square)\";\r\n\tconst string RTD_Version_Develop = \"[ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square)\";\r\n\tInformation($\"Processing {READMEmd} ...\");\r\n    var body = _File_.ReadAllText(READMEmd, System.Text.Encoding.UTF8);\r\n\tvar RTD_IsReplaced = false;\r\n\tif (body.Contains(RTD_Version_Latest))\r\n\t{\r\n\t\tInformation($\"  {READMEmd}: Detected ReadTheDocs LATEST version marker -> {RTD_Version_Latest}\");\r\n\t\tbody = body.Replace(RTD_Version_Latest, RTD_NuGet_Valid_Domain);\r\n\t\tRTD_IsReplaced = true;\r\n\t}\r\n\tif (body.Contains(RTD_Version_Develop))\r\n\t{\r\n\t\tInformation($\"  {READMEmd}: Detected ReadTheDocs DEVELOP version marker -> {RTD_Version_Develop}\");\r\n\t\tbody = body.Replace(RTD_Version_Develop, RTD_NuGet_Valid_Domain);\r\n\t\tRTD_IsReplaced = true;\r\n\t}\r\n\tif (RTD_IsReplaced)\r\n\t{\r\n\t\tInformation($\"  {READMEmd}: ReadTheDocs badge has been replaced with -> {RTD_NuGet_Valid_Domain}\");\r\n\t}\t\r\n\r\n\tconst string IMG_Octocat_HTML = \"<img src=\\\"https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat.png\\\" alt=\\\"octocat\\\" height=\\\"25\\\" />\";\r\n\tconst string IMG_NuGet_Valid_MD = \"![octocat](https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat-25px.png)\";\r\n\tif (body.Contains(IMG_Octocat_HTML))\r\n\t{\r\n\t\tInformation($\"  {READMEmd}: Detected Octocat HTML IMG-tag -> \" + IMG_Octocat_HTML);\r\n\t\tbody = body.Replace(IMG_Octocat_HTML, IMG_NuGet_Valid_MD);\r\n\t\tInformation($\"  {READMEmd}: Octocat HTML IMG-tag has been replaced with -> \" + IMG_NuGet_Valid_MD);\r\n\t}\r\n\tInformation($\"  {READMEmd}: Writing the body of the {READMEmd}...\");\r\n\t_File_.WriteAllText(READMEmd, body, System.Text.Encoding.UTF8);\r\n\tInformation($\"DONE Processing {READMEmd}{NL}\");\r\n}\r\n\r\nprivate void GenerateReport(Cake.Core.IO.FilePath coverageSummaryFile)\r\n{\r\n\tvar dir = _Directory_.GetCurrentDirectory();\r\n\tInformation(\"GenerateReport: Current directory: \" + dir);\r\n\r\n\tvar reportSettings = new ProcessArgumentBuilder();\r\n\treportSettings.Append($\"-targetdir:\" + $\"{dir}/{artifactsForUnitTestsDir}\");\r\n\treportSettings.Append($\"-reports:\" + coverageSummaryFile);\r\n\treportSettings.Append($\"-filefilters:-*.g.cs\"); // silence warnings for source-generated files (e.g. RegexGenerator.g.cs) that are deleted after build\r\n\r\n\tInformation($\"GenerateReport: Resolving net10.0/ReportGenerator.dll ...\");\r\n\tvar toolpath = Context.Tools.Resolve(\"net10.0/ReportGenerator.dll\");\r\n\tInformation($\"GenerateReport: Tool Path: {toolpath.ToString()}\" + NL);\r\n\r\n\tDotNetExecute(toolpath, reportSettings);\r\n}\r\n\r\n/// Gets unique nuget version for this commit\r\nprivate GitVersion GetNuGetVersionForCommit()\r\n{\r\n    GitVersion(new GitVersionSettings{\r\n        UpdateAssemblyInfo = false,\r\n        OutputType = GitVersionOutput.BuildServer,\r\n\t\tVerbosity = IsRunningInCICD() ? GitVersionVerbosity.Minimal : GitVersionVerbosity.Normal,\r\n    });\r\n    return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });\r\n}\r\n\r\n/// Updates project version in all of our projects\r\nprivate void PersistVersion(string committedVersion, string newVersion)\r\n{\r\n\tInformation(string.Format(\"We'll search all csproj files for {0} and replace with {1}...\", committedVersion, newVersion));\r\n\tvar projectFiles = GetFiles(\"./**/*.csproj\")\r\n\t\t.Where(f => !f.FullPath.Contains(\"Ocelot.Samples.\"))\r\n\t\t.ToList();\r\n\tforeach(var projectFile in projectFiles)\r\n\t{\r\n\t\tvar file = projectFile.ToString();\r\n\t\tInformation(string.Format(\"Updating {0}...\", file));\r\n\r\n\t\tvar updatedProjectFile = _File_.ReadAllText(file, System.Text.Encoding.UTF8)\r\n\t\t\t.Replace(committedVersion, newVersion);\r\n\r\n\t\t_File_.WriteAllText(file, updatedProjectFile, System.Text.Encoding.UTF8);\r\n\t}\r\n}\r\n\r\n// Publishes code and symbols packages to nuget feed, based on contents of artifacts file\r\nprivate void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl)\r\n{\r\n\tInformation($\"{nameof(PublishPackages)}: Publishing to NuGet...\");\r\n\tvar artifacts = _File_.ReadAllLines(artifactsFile)\r\n\t\t.Distinct()\r\n\t\t.Where(a => a.EndsWith(\".nupkg\", StringComparison.OrdinalIgnoreCase))\r\n\t\t.ToList();\r\n\tvar skippable = new List<string>\r\n\t{\r\n\t\t\"ReleaseNotes.md\", // skip always\r\n\t\t// \"Ocelot.24.0.0\",\r\n\t\t// \"Ocelot.Cache.CacheManager\",\r\n\t\t// \"Ocelot.Provider.Consul\",\r\n\t\t// \"Ocelot.Provider.Eureka\",\r\n\t\t// \"Ocelot.Provider.Kubernetes\",\r\n\t\t// \"Ocelot.Provider.Polly\",\r\n\t\t// \"Ocelot.Tracing.Butterfly\",\r\n\t\t// \"Ocelot.Tracing.OpenTracing\",\r\n\t};\r\n\tvar includedInTheRelease = new List<string>\r\n\t{\r\n\t\t\"Ocelot.Provider.Kubernetes\",\r\n\t};\r\n\tforeach (var artifact in artifacts)\r\n\t{\r\n\t\tif (skippable.Exists(x => artifact.StartsWith(x, StringComparison.OrdinalIgnoreCase)))\r\n\t\t\tcontinue;\r\n\t\t// if (!includedInTheRelease.Exists(x => artifact.StartsWith(x))) continue;\r\n\r\n\t\tvar package = packagesDir + File(artifact);\r\n\t\tInformation($\"{nameof(PublishPackages)}: Pushing package \" + package + \"...\");\r\n\t\ttry\r\n\t\t{\r\n\t\t\tDotNetNuGetPush(package,\r\n\t\t\t\tnew DotNetNuGetPushSettings { ApiKey = feedApiKey, Source = codeFeedUrl, SkipDuplicate = true });\r\n\r\n\t\t\tvar symbolArtifact = artifact.Replace(\".nupkg\", \".snupkg\");\r\n\t\t\tvar symbolPackage = packagesDir + File(symbolArtifact);\r\n\t\t\tif (FileExists(symbolPackage))\r\n\t\t\t{\r\n\t\t\t\tInformation($\"  Pushing symbol package {symbolPackage}...\");\r\n\t\t\t\tSystem.Threading.Thread.Sleep(1000);\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\tDotNetNuGetPush(symbolPackage,\r\n\t\t\t\t\t\tnew DotNetNuGetPushSettings { ApiKey = feedApiKey, Source = codeFeedUrl, SkipDuplicate = true });\t\t\t\t\t\t\r\n\t\t\t\t}\r\n\t\t\t\tcatch (Exception symEx)\r\n\t\t\t\t{\r\n\t\t\t\t\tWarning($\"  Symbol push failed: {symEx.Message}\");\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tInformation($\"  No symbol package found for {artifact}\");\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch (Exception ex)\r\n\t\t{\r\n\t\t\tInformation(\"--------------------------------------------------------------\");\r\n\t\t\tWarning(ex.ToString());\r\n\t\t\tthrow; // exit task with non-zero result -> failed step -> failed job in Actions\r\n\t\t}\r\n\t\t\t// catch (Exception ex)\r\n\t\t\t// {\r\n\t\t\t// \tWarning(ex.ToString());\r\n\t\t\t// \t// bool isConflict = ex.ToString().Contains(\"409\") || ex.ToString().Contains(\"Conflict\");\r\n\t\t\t// \tif (!isBeta /*|| !isConflict*/) throw;\r\n\r\n\t\t\t// \tvar match = Regex.Match(theArtifact, @\"-beta\\.(\\d+)(?=\\.nupkg$)\");\r\n\t\t\t// \tif (!match.Success)\r\n\t\t\t// \t{\r\n\t\t\t// \t\tWarning(\"  No beta version found in the artifact name, but it should be there. Artifact: \" + theArtifact);\r\n\t\t\t// \t\tbreak;\r\n\t\t\t// \t}\r\n    \t\t// \tvar betaNumber = match.Groups[1].Value;\r\n    \t\t// \tInformation($\"  Detected Beta number: {betaNumber}\");\r\n\t\t\t// \tint newBetaVer = int.Parse(betaNumber) + 1; // increase beta version by 1 trying to find the next free beta number\r\n\t\t\t// \tvar newArtifact = Regex.Replace(theArtifact, @\"-beta\\.\\d+(?=\\.nupkg$)\", \"-beta.\" + newBetaVer);\r\n\t\t\t// \tvar newPackage = packagesDir + File(newArtifact);\r\n\t\t\t// \tif (FileExists(newPackage)) DeleteFile(newPackage);\r\n\t\t\t// \tMoveFile(package, newPackage);\r\n\t\t\t// \tWarning($\"  Package renamed: {package} -> {newPackage} (Attempt #{attempts})\");\r\n\t\t\t// \tvar oldSymbol = packagesDir + File(theArtifact.Replace(\".nupkg\", \".snupkg\"));\r\n\t\t\t// \tvar newSymbol = packagesDir + File(newArtifact.Replace(\".nupkg\", \".snupkg\"));\r\n\t\t\t// \tif (FileExists(oldSymbol))\r\n\t\t\t// \t{\r\n\t\t\t// \t\tif (FileExists(newSymbol)) DeleteFile(newSymbol);\r\n\t\t\t// \t\tMoveFile(oldSymbol, newSymbol);\r\n\t\t\t// \t}\r\n\t\t\t// \tpackage = newPackage;\r\n\t\t\t// \ttheArtifact = newArtifact;\t\t\t\t\r\n\t\t\t// \tSystem.Threading.Thread.Sleep(1000);\r\n\t\t\t// }\r\n\t}\r\n}\r\n\r\nprivate bool PackageExists(string packageId, string version)\r\n{\r\n\tvar url = $\"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/{version}/{packageId.ToLowerInvariant()}.{version}.nupkg\";\r\n\ttry\r\n\t{\r\n\t\tvar response = HttpGet(url);\r\n\t\tInformation($\"{nameof(PackageExists)}: Package ver.{packageId}.{version} exists\");\r\n\t\treturn true;\r\n\t}\r\n\tcatch (Exception ex)\r\n\t{\r\n\t\tWarning(ex.ToString());\r\n\t\tInformation($\"{nameof(PackageExists)}: Package ver.{packageId}.{version} does NOT exist\");\r\n\t}\r\n\treturn false;\r\n}\r\n\r\nprivate void SetupGitHubClient(System.Net.Http.HttpClient client)\r\n{\r\n\tstring token = Environment.GetEnvironmentVariable(\"OCELOT_GITHUB_API_KEY\");\r\n\tclient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(\"Bearer\", token);\r\n\tclient.DefaultRequestHeaders.Add(\"User-Agent\", \"Ocelot Release\");\r\n\tclient.DefaultRequestHeaders.Add(\"Accept\", \"application/vnd.github+json\");\r\n\tclient.DefaultRequestHeaders.Add(\"X-GitHub-Api-Version\", \"2022-11-28\");\r\n}\r\n\r\nprivate dynamic CreateGitHubRelease()\r\n{\r\n\tvar body = ReleaseNotesAsJson();\r\n\tvar json = $\"{{ \\\"tag_name\\\": \\\"{versioning.NuGetVersion}\\\", \\\"target_commitish\\\": \\\"{versioning.BranchName}\\\", \\\"name\\\": \\\"{versioning.NuGetVersion}\\\", \\\"body\\\": \\\"{body}\\\", \\\"draft\\\": true, \\\"prerelease\\\": true, \\\"generate_release_notes\\\": false }}\";\r\n\tvar content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, \"application/json\");\r\n\r\n\tusing (var client = new System.Net.Http.HttpClient())\r\n\t{\t\r\n\t\tSetupGitHubClient(client);\r\n\t\tvar result = client.PostAsync(\"https://api.github.com/repos/ThreeMammals/Ocelot/releases\", content).Result;\r\n\t\tif (result.StatusCode != System.Net.HttpStatusCode.Created) \r\n\t\t{\r\n\t\t\tvar msg = \"CreateGitHubRelease: StatusCode = \" + result.StatusCode;\r\n\t\t\tInformation(msg);\r\n\t\t\tthrow new Exception(msg);\r\n\t\t}\r\n\t\tvar releaseData = result.Content.ReadAsStringAsync().Result;\r\n\t\tdynamic releaseJSON = Newtonsoft.Json.JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JObject>(releaseData);\r\n\t\tInformation(\"CreateGitHubRelease: Release ID is \" + releaseJSON.id);\r\n\t\treturn releaseJSON;\r\n\t}\r\n}\r\n\r\nprivate string ReleaseNotesAsJson()\r\n{\r\n\tvar body = _File_.ReadAllText(releaseNotesFile, System.Text.Encoding.UTF8);\r\n\treturn System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(body);\r\n}\r\n\r\nprivate void UploadFileToGitHubRelease(dynamic release, FilePath file)\r\n{\r\n\tvar data = _File_.ReadAllBytes(file.FullPath);\r\n\tvar content = new System.Net.Http.ByteArrayContent(data);\r\n\tcontent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(\"application/octet-stream\");\r\n\r\n\tusing (var client = new System.Net.Http.HttpClient())\r\n\t{\t\r\n\t\tSetupGitHubClient(client);\r\n\t\tint releaseId = release.id;\r\n\t\tvar fileName = file.GetFilename();\r\n\t\tstring uploadUrl = release.upload_url.ToString();\r\n\t\t// Information($\"UploadFileToGitHubRelease: uploadUrl is {uploadUrl}\");\r\n\t\tstring[] parts = uploadUrl.Replace(\"{\", \"\").Split(',');\r\n\t\tuploadUrl = parts[0] + \"=\" + fileName; // $\"https://uploads.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}/assets?name={fileName}\"\r\n\t\tInformation($\"UploadFileToGitHubRelease: uploadUrl is {uploadUrl}\");\r\n\t\tvar result = client.PostAsync(uploadUrl, content).Result;\r\n\t\tif (result.StatusCode != System.Net.HttpStatusCode.Created) \r\n\t\t{\r\n\t\t\tInformation($\"UploadFileToGitHubRelease: StatusCode is {result.StatusCode}. Release ID is {releaseId}. Failed to upload file '{fileName}' to URL: {uploadUrl}\");\r\n\t\t\tthrow new Exception(\"UploadFileToGitHubRelease: StatusCode is \" + result.StatusCode);\r\n\t\t}\r\n\t}\r\n}\r\n\r\nprivate void CompleteGitHubRelease(dynamic release)\r\n{\r\n\tint releaseId = release.id;\r\n\tstring url = release.url.ToString();\r\n\tstring body = ReleaseNotesAsJson();\r\n\tbool isPreRelease = !IsMainBranch();\r\n\tvar json = $\"{{ \\\"tag_name\\\": \\\"{versioning.NuGetVersion}\\\", \\\"target_commitish\\\": \\\"{versioning.BranchName}\\\", \\\"name\\\": \\\"{versioning.NuGetVersion}\\\", \\\"body\\\": \\\"{body}\\\", \\\"draft\\\": false, \\\"prerelease\\\": {isPreRelease.ToString().ToLower()} }}\";\r\n\tvar request = new System.Net.Http.HttpRequestMessage(new System.Net.Http.HttpMethod(\"Patch\"), url); // $\"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}\");\r\n\trequest.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, \"application/json\");\r\n\r\n\tusing (var client = new System.Net.Http.HttpClient())\r\n\t{\t\r\n\t\tSetupGitHubClient(client);\r\n\t\tvar result = client.SendAsync(request).Result;\r\n\t\tif (result.StatusCode != System.Net.HttpStatusCode.OK) \r\n\t\t{\r\n\t\t\tInformation($\"CompleteGitHubRelease: StatusCode is {result.StatusCode}. Release ID is {releaseId}. Failed to patch release with URL: {url}\");\r\n\t\t\tthrow new Exception(\"CompleteGitHubRelease: StatusCode = \" + result.StatusCode);\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// gets the resource from the specified url\r\nprivate async Task<string> GetResourceAsync(string url)\r\n{\r\n\ttry\r\n\t{\r\n\t\tInformation(\"Getting resource from \" + url);\r\n\r\n\t\tusing var client = new System.Net.Http.HttpClient();\r\n\t\tclient.DefaultRequestHeaders.Accept.ParseAdd(\"application/vnd.github.v3+json\");\r\n\t\tclient.DefaultRequestHeaders.UserAgent.ParseAdd(\"BuildScript\");\r\n\r\n\t\tusing var response = await client.GetAsync(url);\r\n\t\tresponse.EnsureSuccessStatusCode();\r\n\t\tvar content = await response.Content.ReadAsStringAsync();\r\n\t\tInformation(\"Response is >>>\" + NL + content + NL + \"<<<\");\r\n\t\treturn content;\r\n\t}\r\n\tcatch(Exception exception)\r\n\t{\r\n\t\tInformation(\"There was an exception \" + exception);\r\n\t\tthrow;\r\n\t}\r\n}\r\n\r\nprivate bool IsRunningInCICD()\r\n\t=> IsRunningOnCircleCI() || IsRunningInGitHubActions();\r\nprivate bool IsRunningOnCircleCI()\r\n\t=> !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(\"CIRCLECI\"));\r\nprivate bool IsRunningInGitHubActions()\r\n\t=> Environment.GetEnvironmentVariable(\"GITHUB_ACTIONS\") == \"true\";\r\n\r\nprivate bool IsMainBranch()\r\n{\r\n\tvar br = GetBranchName().ToLower();\r\n    return br == \"main\";\r\n}\r\nprivate string GetBranchName()\r\n{\r\n    return versioning?.BranchName ?? GetGitBranch();\r\n}\r\nprivate string GetGitBranch()\r\n{\r\n\tvar lines = GitHelper(\"branch --show-current\");\r\n\tvar branch = string.Join(string.Empty, lines);\r\n\treturn branch ?? \"Unknown Branch\";\r\n}\r\n"
  },
  {
    "path": "codeanalysis.ruleset",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RuleSet Name=\"Rules for StyleCop.Analyzers\" Description=\"Code analysis rules for StyleCop.Analyzers.csproj.\" ToolsVersion=\"17.0\">\n  <Rules AnalyzerId=\"AsyncUsageAnalyzers\" RuleNamespace=\"AsyncUsageAnalyzers\">\n    <Rule Id=\"AvoidAsyncSuffix\" Action=\"None\" />\n    <Rule Id=\"AvoidAsyncVoid\" Action=\"Error\" />\n    <Rule Id=\"UseAsyncSuffix\" Action=\"None\" />\n    <Rule Id=\"UseConfigureAwait\" Action=\"Error\" />\n  </Rules>\n  <Rules AnalyzerId=\"Microsoft.CodeAnalysis.CSharp.Features\" RuleNamespace=\"Microsoft.CodeAnalysis.CSharp.Features\">\n    <Rule Id=\"IDE0003\" Action=\"None\" />\n  </Rules>\n  <Rules AnalyzerId=\"StyleCop.Analyzers\" RuleNamespace=\"StyleCop.Analyzers\">\n    <Rule Id=\"SA0000\" Action=\"Hidden\" />\n    <Rule Id=\"SA1000\" Action=\"None\" />\n    <Rule Id=\"SA1001\" Action=\"None\" />\n    <Rule Id=\"SA1002\" Action=\"None\" />\n    <Rule Id=\"SA1003\" Action=\"None\" />\n    <Rule Id=\"SA1005\" Action=\"None\" />\n    <Rule Id=\"SA1008\" Action=\"None\" />\n    <Rule Id=\"SA1009\" Action=\"None\" />\n    <Rule Id=\"SA1010\" Action=\"None\" />\n    <Rule Id=\"SA1011\" Action=\"None\" />\n    <Rule Id=\"SA1012\" Action=\"None\" />\n    <Rule Id=\"SA1013\" Action=\"None\" />\n    <Rule Id=\"SA1015\" Action=\"None\" />\n    <Rule Id=\"SA1016\" Action=\"None\" />\n    <Rule Id=\"SA1021\" Action=\"Error\" />\n    <Rule Id=\"SA1022\" Action=\"Error\" />\n    <Rule Id=\"SA1024\" Action=\"None\" />\n    <Rule Id=\"SA1026\" Action=\"None\" />\n    <Rule Id=\"SA1028\" Action=\"None\" />\n    <Rule Id=\"SA1100\" Action=\"Error\" />\n    <Rule Id=\"SA1101\" Action=\"None\" />\n    <Rule Id=\"SA1106\" Action=\"None\" />\n    <Rule Id=\"SA1111\" Action=\"None\" />\n    <Rule Id=\"SA1112\" Action=\"None\" />\n    <Rule Id=\"SA1116\" Action=\"None\" />\n    <Rule Id=\"SA1117\" Action=\"None\" />\n    <Rule Id=\"SA1118\" Action=\"None\" />\n    <Rule Id=\"SA1119\" Action=\"Error\" />\n    <Rule Id=\"SA1121\" Action=\"Error\" />\n    <Rule Id=\"SA1122\" Action=\"None\" />\n    <Rule Id=\"SA1128\" Action=\"None\" />\n    <Rule Id=\"SA1133\" Action=\"Error\" />\n    <Rule Id=\"SA1200\" Action=\"None\" />\n    <Rule Id=\"SA1201\" Action=\"None\" />\n    <Rule Id=\"SA1202\" Action=\"None\" />\n    <Rule Id=\"SA1203\" Action=\"None\" />\n    <Rule Id=\"SA1204\" Action=\"None\" />\n    <Rule Id=\"SA1208\" Action=\"None\" />\n    <Rule Id=\"SA1209\" Action=\"None\" />\n    <Rule Id=\"SA1210\" Action=\"None\" />\n    <Rule Id=\"SA1214\" Action=\"None\" />\n    <Rule Id=\"SA1216\" Action=\"None\" />\n    <Rule Id=\"SA1300\" Action=\"None\" />\n    <Rule Id=\"SA1302\" Action=\"Error\" />\n    <Rule Id=\"SA1303\" Action=\"None\" />\n    <Rule Id=\"SA1304\" Action=\"Error\" />\n    <Rule Id=\"SA1309\" Action=\"None\" />\n    <Rule Id=\"SA1310\" Action=\"None\" />\n    <Rule Id=\"SA1311\" Action=\"Error\" />\n    <Rule Id=\"SA1400\" Action=\"None\" />\n    <Rule Id=\"SA1401\" Action=\"None\" />\n    <Rule Id=\"SA1402\" Action=\"None\" />\n    <Rule Id=\"SA1403\" Action=\"Error\" />\n    <Rule Id=\"SA1404\" Action=\"Error\" />\n    <Rule Id=\"SA1405\" Action=\"Error\" />\n    <Rule Id=\"SA1406\" Action=\"Error\" />\n    <Rule Id=\"SA1407\" Action=\"Error\" />\n    <Rule Id=\"SA1408\" Action=\"None\" />\n    <Rule Id=\"SA1410\" Action=\"Error\" />\n    <Rule Id=\"SA1411\" Action=\"Error\" />\n    <Rule Id=\"SA1500\" Action=\"None\" />\n    <Rule Id=\"SA1502\" Action=\"None\" />\n    <Rule Id=\"SA1516\" Action=\"None\" />\n    <Rule Id=\"SA1600\" Action=\"None\" />\n    <Rule Id=\"SA1602\" Action=\"None\" />\n    <Rule Id=\"SA1603\" Action=\"Error\" />\n    <Rule Id=\"SA1609\" Action=\"Error\" />\n    <Rule Id=\"SA1611\" Action=\"None\" />\n    <Rule Id=\"SA1623\" Action=\"None\" />\n    <Rule Id=\"SA1633\" Action=\"None\" />\n    <Rule Id=\"SA1636\" Action=\"Error\" />\n    <Rule Id=\"SA1642\" Action=\"Error\" />\n    <Rule Id=\"SA1643\" Action=\"Error\" />\n    <Rule Id=\"SA1652\" Action=\"None\" />\n  </Rules>\n</RuleSet>"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    project: #add everything under here, more options at https://docs.codecov.com/docs/commit-status\n      default:\n        target: auto\n        threshold: 0%\n        base: auto\n\ncomment:\n  layout: \"reach, diff, flags, tree\"\n  behavior: default\n\ngithub_checks:\n  annotations: true\n"
  },
  {
    "path": "coverlet.runsettings",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RunSettings>\n  <DataCollectionRunSettings>\n    <DataCollectors>\n      <DataCollector friendlyName=\"XPlat Code Coverage\">\n        <Configuration>\n          <Format>cobertura</Format>\n          <!-- Exclude the Ocelot.Testing assembly (shared testing library, not a product under test) -->\n          <Exclude>[Ocelot.Testing*]*</Exclude>\n          <!-- Do not count the test assemblies themselves toward coverage -->\n          <IncludeTestAssembly>false</IncludeTestAssembly>\n          <!-- Exclude assemblies that have no matching source files (e.g. generated or third-party) -->\n          <ExcludeAssembliesWithoutSources>MissingAll</ExcludeAssembliesWithoutSources>\n        </Configuration>\n      </DataCollector>\n    </DataCollectors>\n  </DataCollectionRunSettings>\n</RunSettings>\n"
  },
  {
    "path": "docker/Dockerfile.base",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine\n\nRUN apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib git openssh-client\n\n# Install .NET 8 SDK\nRUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh\nRUN chmod u+x ./dotnet-install.sh \nRUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet\n\n# Generate and export the development SSL certificate\nRUN mkdir -p /certs\nRUN dotnet dev-certs https -ep /certs/cert.pem -p ''\nRUN chmod 644 /certs/cert.pem\n\nENV ASPNETCORE_URLS=\"https://+;http://+\" \nENV ASPNETCORE_HTTPS_PORT=443 \nENV ASPNETCORE_Kestrel__Certificates__Default__Password=\"\" \nENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pem\n"
  },
  {
    "path": "docker/Dockerfile.build",
    "content": "# call from ocelot repo root with\n# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build .\n# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build .\n\nFROM ocelot2/circleci-build:latest\n\nARG OCELOT_COVERALLS_TOKEN\n\nENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN\n\nWORKDIR /build\n\nCOPY ./. .\n\nRUN dotnet tool restore\n\nRUN\tdotnet cake\n"
  },
  {
    "path": "docker/Dockerfile.release",
    "content": "# call from ocelot repo root with\n# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build .\n# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build .\n\nFROM ocelot2/circleci-build:latest\n\nARG OCELOT_COVERALLS_TOKEN\nARG OCELOT_NUTGET_API_KEY\nARG OCELOT_GITHUB_API_KEY\n\nENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN\nENV OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY\nENV OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY\n\nWORKDIR /build\n\nCOPY ./. .\n\nRUN dotnet tool restore\n\nRUN\tdotnet cake\n"
  },
  {
    "path": "docker/Dockerfile.windows",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:9.0-nanoserver-ltsc2022\n\n# Install PowerShell globally  AND  RUN powershell -Command \"gci\"\n#RUN dotnet tool install -g powershell  &&  pwsh -Command \"gci\"\nRUN pwsh -Command \"gci\"\n\n# # Install .NET 8 SDK\nUSER ContainerAdministrator\nRUN pwsh -Command \"Invoke-WebRequest -OutFile dotnet-install.ps1 https://dot.net/v1/dotnet-install.ps1; gci\"\nENV DOTNET_GENERATE_ASPNET_CERTIFICATE=true\nRUN pwsh -Command \"./dotnet-install.ps1 -Channel 8.0 -InstallDir 'C:\\Program Files\\dotnet\\'; gci -Path C:\\\\ -Recurse -Include *8.0* -Directory\"\n\n#RUN git status\nRUN dotnet --info\n\n# Generate and export the development SSL certificate\n#RUN mkdir -p certs\n#RUN dotnet dev-certs https -ep certs/cert.pem -p ''\n#RUN pwsh -Command \"dotnet dev-certs https --clean; dotnet dev-certs https; exit 0;\"\n#RUN pwsh -Command \"dotnet dev-certs https --trust; exit 0;\"\nRUN pwsh -Command \"dotnet dev-certs https --check --trust; exit 0;\"\n\n#ENV ASPNETCORE_URLS=\"https://+;http://+\"  ASPNETCORE_HTTPS_PORT=443\n#ENV ASPNETCORE_Kestrel__Certificates__Default__Password=\"\"  ASPNETCORE_Kestrel__Certificates__Default__Path=certs/cert.pem\n"
  },
  {
    "path": "docker/README.md",
    "content": "# docker build\r\n\r\nThis folder contains the `Dockerfile.*` and `build-*.sh` scripts to create the Ocelot build image & container.\r\n\r\n## Account\r\n- Docker Hub | [Ocelot Gateway](https://hub.docker.com/u/ocelot2)\r\n\r\n## Repositories\r\n- [circleci-build](https://hub.docker.com/r/ocelot2/circleci-build)\r\n\r\n## Outdated Tags\r\n- [8.21.0](https://hub.docker.com/layers/ocelot2/circleci-build/8.21.0/images/sha256-edb46d37ab52d39a5b27dc63895e5944d4d491d1788744ed144ecb4303b94532?context=explore), uploaded on Nov 20, 2023. It contains .NET 8, 7 and 6 SDKs. It supports builds for `net6.0`, `net7.0` and `net8.0` frameworks.\r\n- [8.23.2](https://hub.docker.com/layers/ocelot2/circleci-build/8.23.2/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore), uploaded on Apr 3, 2024. It supports development SSL certificates by the command `dotnet dev-certs https`.\r\n- [latest](https://hub.docker.com/layers/ocelot2/circleci-build/latest/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore) is version 8.23.2, uploaded on Apr 3, 2024.\r\n\r\n## .NET 8-9 Tags\r\n\r\n### Single SDK Tags\r\n- [sdk-8-alpine-lin.net8](https://hub.docker.com/layers/ocelot2/circleci-build/sdk-8-alpine-lin.net8/images/sha256-17c21438771641ba2a3320a6c13fe756a851d707a925188d9616485bfc757b22?context=explore), uploaded on Dec 3, 2024. This Linux OS image contains .NET 8 SDK only. It supports builds for `net8.0` framework.\r\n- [sdk-9-alpine-lin.net9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk-9-alpine-lin.net9/images/sha256-c20d82a52c1a7eebf86bcd32751960ae189e6c50575959ee0499b1d88541c9ea?context=explore), uploaded on Dec 3, 2024. This Linux OS image contains .NET 9 SDK only. It supports builds for `net9.0` framework.\r\n- [sdk8-nanoserver2022-win.net8](https://hub.docker.com/layers/ocelot2/circleci-build/sdk8-nanoserver2022-win.net8/images/sha256-20f21f4361d18301bc885e1f01ebefcd6b024802130db1296173cdfaac5d75e6?context=explore), uploaded on Dec 3, 2024. This Windows OS image contains .NET 8 SDK only. It supports builds for `net8.0` framework.\r\n- [sdk9-nano2022-win.net9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-nano2022-win.net9/images/sha256-37b718885f8cfb3480299f48044836f01aec2370e331667dd1811a4c94a4ce45?context=explore), uploaded on Dec 3, 2024. This Windows OS image contains .NET 9 SDK only. It supports builds for `net9.0` framework.\r\n\r\n### Double SDKs Tags\r\n- [sdk9-alpine-lin.net8-9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-alpine-lin.net8-9/images/sha256-707924d144248178caff80578649f7d0e9b7c70ed51b6fb5171170c9a30a4eae?context=explore) aka [9.24.0](https://hub.docker.com/layers/ocelot2/circleci-build/9.24.0/images/sha256-707924d144248178caff80578649f7d0e9b7c70ed51b6fb5171170c9a30a4eae?context=explore), uploaded on Dec 3, 2023. This Linux OS image contains .NET 8 and 9 SDKs. It supports builds for `net8.0` and `net9.0` frameworks.\r\n- [sdk9-nano2022-win.net8-9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-nano2022-win.net8-9/images/sha256-6e44a8fc52ab091ea43090b6ab182f7a05d7ac31402ce742a7e035323e584cf7?context=explore) aka [9.24.win](https://hub.docker.com/layers/ocelot2/circleci-build/9.24.win/images/sha256-6e44a8fc52ab091ea43090b6ab182f7a05d7ac31402ce742a7e035323e584cf7?context=explore), uploaded on Dec 4, 2023. This Windows OS image contains .NET 8 and 9 SDKs. It supports builds for `net8.0` and `net9.0` frameworks.\r\n\r\n### Links\r\n- Docker Hub | [Microsoft dotnet Images](https://hub.docker.com/r/microsoft/dotnet)\r\n- GitHub | dotnet-docker | [.NET SDK](https://github.com/dotnet/dotnet-docker/blob/main/README.sdk.md)\r\n- StackOverflow | [PowerShell and process exit codes](https://stackoverflow.com/questions/57468522/powershell-and-process-exit-codes)\r\n"
  },
  {
    "path": "docker/build-windows.sh",
    "content": "version=9.24.win\ntag=sdk9-nano2022-win.net8-9\n\ndocker build --no-cache --platform windows/amd64 -t ocelot2/circleci-build -f Dockerfile.windows .\n\ndocker tag ocelot2/circleci-build ocelot2/circleci-build:$tag\ndocker push ocelot2/circleci-build:$tag\n\ndocker tag ocelot2/circleci-build ocelot2/circleci-build:$version\ndocker push ocelot2/circleci-build:$version\n"
  },
  {
    "path": "docker/build.sh",
    "content": "# This script builds the Ocelot Docker file\n# echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin\n\n# {DotNetSdkVer}.{OcelotVer} -> {.NET9}.{24.0} -> 9.24.0\n#version=9.24.0\ntag=sdk9-alpine-lin.net8-9\n\ndocker build --platform linux/amd64 -t ocelot2/circleci-build -f Dockerfile.base .\ndocker tag ocelot2/circleci-build ocelot2/circleci-build:$tag\ndocker push ocelot2/circleci-build:$tag\n# docker tag ocelot2/circleci-build ocelot2/circleci-build:$version\n# docker push ocelot2/circleci-build:$version\n"
  },
  {
    "path": "docker/outdated/Dockerfile.8.21.0.base",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine\n\nRUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib git openssh-client\n\nRUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh\n\nRUN chmod u+x ./dotnet-install.sh \n\n# Install .NET 8 SDK (already included in the base image, but listed for consistency)\nRUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet\n\n# Install .NET 7 SDK\nRUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet\n\n# Install .NET 6 SDK\nRUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet\n"
  },
  {
    "path": "docker/outdated/Dockerfile.8.23.2.base",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine\n\nRUN apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib git openssh-client\n\nRUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh\n\nRUN chmod u+x ./dotnet-install.sh \n\n# Install .NET 8 SDK (already included in the base image, but listed for consistency)\nRUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet\n\n# Install .NET 7 SDK\nRUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet\n\n# Install .NET 6 SDK\nRUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet\n\n# Generate and export the development certificate\nRUN dotnet dev-certs https -ep /certs/cert.pem -p '' && \\\n    chmod 644 /certs/cert.pem\n\nENV ASPNETCORE_URLS=\"https://+;http://+\" \nENV ASPNETCORE_HTTPS_PORT=443 \nENV ASPNETCORE_Kestrel__Certificates__Default__Password=\"\" \nENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pem\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/_static/overrides.css",
    "content": "blockquote {\n    font-size: 0.9em;\n}\naside.footnote-list {\n    font-size: 0.9em;\n}\n.img-valign-middle\n{\n    vertical-align: middle;\n}\n.img-valign-bottom\n{\n    vertical-align: bottom;\n}\n.img-valign-textbottom\n{\n    vertical-align: text-bottom;\n}\n"
  },
  {
    "path": "docs/building/building.rst",
    "content": ".. role:: htm(raw)\n  :format: html\n.. role:: pdf(raw)\n  :format: latex pdflatex\n.. _Ocelot: https://github.com/ThreeMammals/Ocelot\n.. _Cake: https://cakebuild.net\n.. _Bash: https://www.gnu.org/software/bash\n.. _build.cake: https://github.com/ThreeMammals/Ocelot/blob/main/build.cake\n.. _GitHub Actions: https://docs.github.com/en/actions\n.. _NuGet: https://www.nuget.org/profiles/ThreeMammals\n\nBuilding\n========\n\nThis document summarises the build and release process for the `Ocelot`_ project.\nThe build scripts are written using `Cake`_ (C# Make), with relevant build tasks defined in the '`build.cake`_' file located in the root of the `Ocelot`_ project.\nThe scripts are designed to be run by developers locally in a `Bash`_ terminal (on any OS), in Command Prompt (CMD) or PowerShell consoles (on Windows OS),\nor by a CI/CD server (currently `GitHub Actions`_), with minimal logic defined in the build server itself.\n\nThe final goal of the build process is to create ``Ocelot.*`` `NuGet`_ packages (.nupkg files) for redistribution via the `NuGet`_ repository or manually.\nThe build process consists of several steps: (1) compilation, (2) testing, (3) creating and publishing `NuGet`_ packages, and (4) making an official GitHub release.\nThe build process requires pre-installed .NET SDKs on the build machine (host) for all target framework monikers: TFMs are ``net8.0`` and ``net9.0`` currently.\nIn general, the build process is the same across all environments and tools, with a few differences described below.\n\n.. _b-in-ide:\n\nIn IDE\n------\n.. _Release configuration: https://learn.microsoft.com/en-us/visualstudio/debugger/how-to-set-debug-and-release-configurations?view=vs-2022\n\nIn an IDE, a DevOps engineer can build the project in Visual Studio IDE or another IDE in `Release configuration`_ mode, but the latest .NET 8/9 SDKs must be pre-installed on the local machine.\nHowever, this approach is not practical because the generated '.nupkg' files must be uploaded to `NuGet`_ manually, and the GitHub release must also be created manually.\nA better approach is to utilize the '`build.cake`_' script :ref:`b-in-terminal`, which covers all building scenarios.\n\n.. _b-in-terminal:\n\nIn terminal\n-----------\n.. _./: https://github.com/ThreeMammals/Ocelot/tree/main/\n\n  Folder: `./`_\n\nThese are local machine or remote server building scenarios using build scripts, aka '`build.cake`_'.\nIn these scenarios, the following two commands should be run in a terminal from the project's root folder:\n\n.. code-block:: shell\n\n  dotnet tool restore && dotnet cake  # In Bash terminal\n  dotnet tool restore; dotnet cake  # In PowerShell terminal\n\n.. _break: http://break.do\n\n  **Note**: The default target task (\"Default\") is \"Build\", and output files will be stored in the ``./artifacts`` directory.\n\nTo run a desired target task, you need to specify its *name*:\n\n.. code-block:: shell\n\n  dotnet tool restore && dotnet cake --target=name  # In Bash terminal\n  dotnet tool restore; dotnet cake --target=name  # In PowerShell terminal\n\nFor example,\n\n- .. code-block:: shell\n\n    dotnet cake --target=Build\n\n  It runs a local build, performing compilation and testing only.\n\n- .. code-block:: shell\n\n    dotnet cake --target=Version\n  \n  It checks the next version to be tagged in the Git repository during the next release, without performing compilation or testing tasks.\n\n- .. code-block:: shell\n\n    dotnet cake --target=CreateReleaseNotes\n  \n  It generates Release Notes artifacts in the ``/artifacts/Packages`` folder using the ``ReleaseNotes.md`` template file.\n\n- .. code-block:: shell\n\n    dotnet cake --target=Release\n\n  It creates a release, consisting of the following steps: compilation, testing, generating release notes, creating .nupkg files, publishing `NuGet`_ packages, and finally, making a GitHub release.\n\n.. _dotnet-tools.json: https://github.com/ThreeMammals/Ocelot/blob/main/.config/dotnet-tools.json\n\n  **Note 1**: The building tools for the ``dotnet tool restore`` command are configured in the `dotnet-tools.json`_ file.\n\n  **Note 2**: Some targets (build tasks) require appropriate environment variables to be defined directly in the terminal session (aka secret tokens).\n\n.. _b-with-docker:\n\nWith Docker\n-----------\n.. _docker: https://github.com/ThreeMammals/Ocelot/tree/main/docker\n.. _Dockerfile.build: https://github.com/ThreeMammals/Ocelot/blob/main/docker/Dockerfile.build\n.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\n\n  Folder: ./`docker`_\n\nThe best way to replicate the CI/CD process and build `Ocelot`_ locally is by using the `Dockerfile.build`_ file, which can be found in the '`docker`_' folder in the `Ocelot`_ root directory.\nFor example, use the following command:\n\n.. code-block:: shell\n\n  docker build --platform linux/amd64 -f ./docker/Dockerfile.build .\n\nYou may need to adjust the platform flag depending on your system.\n\n  **Note**: This approach is somewhat excessive, but it will work if you are a masterful Docker user. 🙂\n  The Ocelot team has not followed this approach since version `24.0`_, favoring :ref:`b-with-ci-cd`-based builds and occasionally building :ref:`b-in-terminal` instead.\n\n.. _b-with-ci-cd:\n\nWith CI/CD\n----------\n.. _workflows: https://github.com/ThreeMammals/Ocelot/tree/main/.github/workflows \n.. _PR: https://github.com/ThreeMammals/Ocelot/actions/workflows/pr.yml\n.. _Develop: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml\n.. _Release: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml\n.. _Coveralls: https://coveralls.io\n.. |ReleaseButton| image:: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml/badge.svg\n  :target: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml\n  :alt: Release Status\n  :class: img-valign-textbottom\n.. |DevelopButton| image:: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml/badge.svg\n  :target: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml\n  :alt: Development Status\n  :class: img-valign-textbottom\n.. |DevelopCoveralls| image:: https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=develop\n  :target: https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop\n  :alt: Coveralls Status\n  :class: img-valign-textbottom\n.. |ReleaseCoveralls| image:: https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=main\n  :target: https://coveralls.io/github/ThreeMammals/Ocelot?branch=main\n  :alt: Coveralls Status\n  :class: img-valign-textbottom\n.. _break2: http://break.do\n\n  | Folder: ./.github/`workflows`_\n  | Provider: `GitHub Actions`_\n  | Workflows: `PR`_, `Develop`_, `Release`_\n  | Dashboard: `Workflow runs <https://github.com/ThreeMammals/Ocelot/actions>`_ (Actions tab)\n\nThe `Ocelot`_ project utilizes `GitHub Actions`_ as a CI/CD provider, offering seamless integrations with the GitHub ecosystem and APIs.\nStarting from version `24.0`_, all pull requests, development commits, and releases are built using `GitHub Actions`_ workflows.\nThere are three `workflows`_: one for pull requests (`PR`_), one for the ``develop`` branch (`Develop`_), and one for the ``main`` branch (`Release`_).\n\n  **Note**: Each workflow has a dedicated status badge in the `Ocelot README`_:\n  the |ReleaseButton|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml}{Release}` button and\n  the |DevelopButton|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml}{Develop}` button,\n  with the `PR`_ status being published directly in a pull request under the \"Checks\" tab.\n\nThe `PR`_ workflow will track code coverage using `Coveralls`_.\nAfter opening a pull request or submitting a new commit to a pull request, `Coveralls`_ will publish a short message with the current code coverage once the top commit is built.\nConsidering that `Coveralls`_ retains the entire history but does not fail the build if coverage falls below the threshold, all workflows have a built-in 80% threshold,\napplied internally within the ``build-cake`` job, particularly during the \"`Cake Build`_\" step-action.\nIf the code coverage of a newly opened pull request drops below the 80% threshold, the `'build-cake' job`_ will fail, logging an appropriate message in the \"`Cake Build`_\" step.\n\n  **Note 1**: There are special code coverage badges in `Ocelot README`_: the `Develop`_ |DevelopCoveralls| button and the `Release`_ |ReleaseCoveralls| button.\n\n  **Note 2**: The current code coverage of the `Ocelot`_ project is around 85-86%. The coverage threshold is subject to change in upcoming releases.\n  All `Coveralls`_ builds can be viewed by navigating to the `ThreeMammals/Ocelot <https://coveralls.io/github/ThreeMammals/Ocelot>`_ project on Coveralls.io.\n\nDocumentation\n-------------\n.. _docs: https://github.com/ThreeMammals/Ocelot/tree/main/docs\n.. _.readthedocs.yaml: https://github.com/ThreeMammals/Ocelot/blob/main/.readthedocs.yaml\n.. _Read the Docs: https://about.readthedocs.com\n.. _Ocelot app: https://app.readthedocs.org/projects/ocelot/\n.. _README: https://github.com/ThreeMammals/Ocelot/blob/main/docs/readme.md\n.. _Ocelot README: https://github.com/ThreeMammals/Ocelot/blob/main/README.md\n.. |ReleaseDocs| image:: https://readthedocs.org/projects/ocelot/badge/?version=latest&style=flat-square\n  :target: https://app.readthedocs.org/projects/ocelot/builds/?version__slug=latest\n  :alt: ReadTheDocs Status\n  :class: img-valign-middle\n.. |DevelopDocs| image:: https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square\n  :target: https://app.readthedocs.org/projects/ocelot/builds/?version__slug=develop\n  :alt: ReadTheDocs Status\n  :class: img-valign-middle\n.. _break3: http://break.do\n\n  | Folder: ./`docs`_\n  | Dashboard: `Ocelot app`_ project\n\nDocumentation building is configured using the '`.readthedocs.yaml`_' integration file, which allows builds to run separately via the `Read the Docs`_ publisher.\nAll build artifacts and document sources are located in the '`docs`_' folder.\nMore details on the documentation build process can be found in the `README`_.\n\n  **Note 1**: Documentation builds have a dedicated status badges in `Ocelot README`_: the `Develop`_ |DevelopDocs| button and the `Release`_ |ReleaseDocs| button.\n\n  **Note**: Documentation can be easily built locally in a terminal from the '`docs`_' folder by running the ``make.sh`` or ``make.bat`` scripts.\n  The resulting documentation build files will be located in the ``./docs/_build`` folder, with the HTML documentation specifically written to the ``./docs/_build/html`` folder.\n\n.. _b-testing:\n\nTesting\n-------\n\nThe tests should run and function correctly as part of the *building* process using the ``dotnet test`` command.\nYou can also run them in Visual Studio IDE within the Test Explorer window.\nDepending on your build scenario, `Ocelot`_ *testing* can be performed as follows.\n\n:ref:`b-in-ide`: Simply run tests via the Test Explorer window of Visual Studio IDE.\n\n:ref:`b-in-terminal`: There are two main approaches:\n\n1. Run the ``dotnet test`` command to perform all tests (unit, integration, and acceptance):\n\n   .. code-block:: shell\n\n      dotnet test -f net8.0 ./Ocelot.sln\n\n   Or run tests separately per project:\n\n   .. code-block:: shell\n\n      dotnet test -f net8.0 ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj  # Unit tests only\n      dotnet test -f net8.0 ./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj  # Integration tests only\n      dotnet test -f net8.0 ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj  # Acceptance tests only\n\n2. Run ``dotnet cake`` command: ``dotnet cake --target=Tests`` to perform all tests (unit, integration and acceptance).\n   Or run tests separately per *testing* project:\n\n   .. code-block:: shell\n\n      dotnet cake --target=UnitTests # unit tests only\n      dotnet cake --target=IntegrationTests # integration tests only\n      dotnet cake --target=AcceptanceTests # acceptance tests only\n\n:ref:`b-with-docker`: This approach is not recommended.\nInstead, perform automated testing :ref:`b-with-ci-cd` or opt for :ref:`b-in-terminal`-based testing, which is a more advanced method.\n\n:ref:`b-with-ci-cd`: In `GitHub Actions`_ `workflows`_, the *testing* process consists of separate testing steps, organized per job:\n\n* In the `'build' job`_: There are '`Unit Tests`_', '`Integration Tests`_', and '`Acceptance Tests`_' steps.\n* In the `'build-cake' job`_: There is a '`Cake Build`_' step responsible for performing tests internally.\n\n.. _'build' job: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+build%3A+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n.. _Unit Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Unit+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n.. _Integration Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Integration+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n.. _Acceptance Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Acceptance+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n.. _'build-cake' job: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22-cake%3A%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n.. _Cake Build: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22cake-build%2F%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code\n\nSSL certificate\n---------------\n\nTo create a certificate for :ref:`b-testing`, you can use `OpenSSL <https://www.openssl.org/>`_:\n\n* Install the `openssl <https://github.com/openssl/openssl>`__ package (if you are using Windows, download the binaries `here <https://www.openssl.org/source/>`_).\n* Generate a private key:\n\n  .. code-block:: bash\n\n    openssl genrsa 2048 > private.pem\n\n* Generate a self-signed certificate:\n\n  .. code-block:: bash\n\n    openssl req -x509 -days 1000 -new -key private.pem -out public.pem\n\n* If needed, create a PFX file:\n\n  .. code-block:: bash\n\n    openssl pkcs12 -export -in public.pem -inkey private.pem -out mycert.pfx\n\n"
  },
  {
    "path": "docs/building/devprocess.rst",
    "content": ".. _Gitflow Workflow: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow\n.. _GitHub Flow: https://docs.github.com/en/get-started/using-github/github-flow\n.. _develop: https://github.com/ThreeMammals/Ocelot/tree/develop\n.. _main: https://github.com/ThreeMammals/Ocelot/tree/main\n.. _issue(s): https://github.com/ThreeMammals/Ocelot/issues\n.. _discussion: https://github.com/ThreeMammals/Ocelot/discussions\n.. _fork: https://docs.github.com/en/get-started/quickstart/fork-a-repo\n.. _unit: https://github.com/ThreeMammals/Ocelot/tree/develop/test/Ocelot.UnitTests\n.. _acceptance: https://github.com/ThreeMammals/Ocelot/tree/develop/test/Ocelot.AcceptanceTests\n.. _documentation: https://github.com/ThreeMammals/Ocelot/tree/develop/docs\n.. _feature: https://github.com/ThreeMammals/Ocelot/tree/develop/docs/features\n.. _Actions: https://github.com/ThreeMammals/Ocelot/actions\n.. _Coveralls check: https://coveralls.io/github/ThreeMammals/Ocelot\n\nDevelopment Process\n===================\n\n* The *development process* is optimized when using Gitflow branching, as detailed here: `Gitflow Workflow`_.\n  It's important to note that the Ocelot team does not utilize `GitHub Flow`_, which, despite being quicker, does not align with the efficiency required for Ocelot's delivery.\n* Contributors are free to manage their pull requests and feature branches as they see fit to contribute to the '`develop`_' branch.\n* Maintainers have the autonomy to handle pull requests and merges. Any merges to the '`main`_' branch will trigger the release of packages to GitHub and NuGet.\n* In conclusion, while users should adhere to the guidelines in :doc:`../building/devprocess`, maintainers should follow the procedures outlined in :doc:`../building/releaseprocess`.\n\nStages\n------\n\nOcelot project follows this *development process* to integrate work into a merged commit in the '`develop`_' branch:\n\n1. Users either create a new issue or select an existing `issue(s)`_ on GitHub.\n   Issues can also be generated from `discussion`_ topics when necessary and agreed upon.\n\n2. Users should create a `fork`_ and branch off of it (unless they are a core team member, in which case they can branch directly from the main/head/upstream repository), e.g., ``feature/xxx``, ``bug/xxx``, etc.\n   The \"xxx\" can be the issue number or a brief description.\n\n3. Once contributors are satisfied with their work, they can submit a pull request against the `develop`_ branch on GitHub with their changes.\n\n4. The Ocelot team will review the pull request and, if satisfactory, merge it; otherwise, they will provide feedback for the contributor to address.\n   To expedite pull request approval, contributors should consider:\n\n   - Ensuring all changes are covered by `unit`_ and `acceptance`_ tests.\n   - Ensuring that the code coverage percentage from `unit`_ tests does not decrease; thus, the `Coveralls check`_ reports a green status.\n   - Updating any `documentation`_ affected by the changes, with a required review of the appropriate `feature`_ document.\n   - Verifying that the feature is necessary and does not duplicate existing Ocelot features.\n\n5. A pull request must meet the following criteria before merging:\n\n   - All new code must be covered by `unit`_ tests.\n   - There must be at least one `acceptance`_ test for the happy path of the new code.\n   - Tests must pass locally, in Visual Studio Test Explorer or in terminal after performing ``dotnet test`` command.\n   - The build must have a green status on repository `Actions`_ as passed *checks* of the pull request (aka `Checks`_ tab).\n   - The build's performance must not be significantly degraded on repository `Actions`_ page for `PR`_ workflow.\n   - The main `Ocelot package`_ must not introduce any non-Microsoft `dependencies`_.\n\n.. _PR: https://github.com/ThreeMammals/Ocelot/actions/workflows/pr.yml\n.. _Checks: https://github.com/ThreeMammals/Ocelot/pull/2283/checks\n.. _Ocelot package: https://www.nuget.org/packages/Ocelot\n.. _dependencies: https://www.nuget.org/packages/Ocelot#dependencies-body-tab\n.. _NuGet packages: https://www.nuget.org/profiles/ThreeMammals\n\n6. Once the pull request is merged with *\"Squash and Merge\"* option into the `develop`_ branch, the ``Ocelot.*`` `NuGet packages`_ will not be updated until a release is crafted.\n   The concluding step involves returning to GitHub to close any resolved `issue(s)`_.\n\nNotes\n-----\n\n**Note 1**: The `issue(s)`_ linked to the pull request within the *Development* settings (on the right sidebar of the pull request settings) will automatically close upon merging.\nIt is crucial for developers to utilize the *\"Link an issue from this repository\"* feature in the *Development* settings.\nAn alternative way to link `issue(s)`_ is by specifying them in the pull request description, where the developer lists the linked `issue(s)`_ that need to be closed.\nFor example:\n\n.. code-block:: markdown\n\n  ## Fixes #1222\n  - #1222\n\n  ## Closes #1333\n  - #1333\n\n  ## Proposed Changes\n  - change 1\n  - change 2  \n\nThis Markdown should automatically link the desired bug/issue in the open status.\nFor bugs, the developer needs to write \"Fixes #xxxx\", and for features, \"Closes #xxxx\".\n\n.. _GitHub Actions: https://docs.github.com/en/actions\n.. _workflows: https://github.com/ThreeMammals/Ocelot/tree/main/.github/workflows \n\n**Note 2**: All pull request builds are conducted using `GitHub Actions`_, but developers have the freedom to build Ocelot as needed.\nDetails can be found in the :doc:`../building/building` chapter.\nAdditionally, for a deeper understanding of the current Ocelot CI/CD environment and a clearer view of the CI/CD build process, refer to the \"Building :ref:`b-with-ci-cd`\" section.\n\n**Note 3**: Should you encounter any confusion or obstacles, do not hesitate to reach out to the members of the 'Ocelot Team' or the repository maintainers.\n\n.. _dev-best-practices:\n\nBest Practices\n--------------\n* Refer to the Ocelot `Actions`_ dashboard on GitHub to verify the latest build statuses for the three current `workflows`_.\n  It is recommended to monitor the build status of each workflow on the `Actions`_ dashboard or directly in the `Checks`_ tab of a pull request.\n  If a build fails, initiate a new build by pushing a new commit, or consult with online maintainers or code reviewers to ensure the current pull request build is successful.\n* Request a code review after reaching the \"Development Complete\" stage, and address all feedback issues.\n  Code is deemed complete when robust code, relevant `unit`_ and `acceptance`_ tests, and `documentation`_ updates are in place.\n* Set up your development environment on Windows OS using Visual Studio IDE.\n  While development in Linux OS with alternative IDEs is possible, it is not recommended. For more details, refer to the :ref:`dev-fun` section.\n* Remain online after submitting a pull request/issue to ensure maintainers can reach you promptly.\n  Note that if you are offline for extended periods, such as days, weeks, or months, maintainers may deprioritize your work.\n  A strong contribution ethic implies constant online presence and proactivity.\n\n.. _dev-fun:\n\nDev Fun\n-------\n\nThis section is part of the :ref:`dev-best-practices` and is written to be more amusing D)\n\nEOL Gotchas\n^^^^^^^^^^^\n\n  *Also known as, \"Line-Endings problem\"*\n\n  Since the project's inception in 2016, this issue has been persistent.\n  Indeed, some lines end with the ``LF`` character, typical of the Linux OS.\n  Many of our contributors work on Linux and use IDEs like Visual Studio Code, JetBrains .NET Rider, which defaults to the ``LF`` as the newline character.\n  As a result, we have numerous files with inconsistent or mixed EOL characters.\n\nThis problem stems from the well-known dilemma of End-of-Line (EOL) characters in cross-OS development.\nFor the Windows OS, the EOL character is ``CRLF``, while for Linux, it is ``LF``.\nModern IDEs and Git repositories have their own strategies for detecting inconsistencies of mixed EOLs in source files.\nHowever, the GitHub \"Files Changed\" tool unfortunately registers a line change in two scenarios: ``CRLF`` to ``LF`` and ``LF`` to ``CRLF``, even when there's no actual code change!\nReviewing such pull requests with fictitious (\"fake\") changes is always challenging because the reviewer's focus should be on actual code changes.\n\n  Please note, if a pull request is filled with \"fake\" changes in *\"Files Changed\"*, the code reviewer has the right to not provide a code review, mark the PR as a draft, or even close it.\n\nOur standard practice is to maintain end-of-line characters as they are.\nMoreover, we utilize Visual Studio's unique ``.editorconfig`` IDE analyzer settings for EOL to avoid issues with line endings.\nThese settings are specific to Visual Studio, hence we recommend rebasing a feature branch onto develop using Visual Studio exclusively.\n\n    Special EOL settings can be specified in the ``.gitattributes`` file of the git repository, although we do not currently manage this.\n\nOur current recommendations for addressing the end-of-line (EOL) issue are as follows:\n\n* Ideally, resolve merge conflicts by prioritizing the changes in the `develop`_ branch, then manually incorporate your changes in the merge tool dialog.\n  It appears that changes from the feature branch are being included, even if they are minor.\n  Conflicts should be addressed by manually applying your changes to the `develop`_ branch with a merge tool.\n\n* If changes from the feature branch are given priority (despite being minor), the merge tool will document them and apply ``CRLF`` end-of-line characters according to the rules specified in ``.editorconfig``.\n  This is the source of the issue.\n\n* Renaming a method in an IDE, such as Visual Studio, or using another auto-refactoring command, causes Visual Studio to apply the command using the default styling rules in ``.editorconfig``, which includes `CRLF settings <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20end_of_line&type=code>`_.\n  Thus, applying auto-refactoring commands inadvertently alters the EOL characters, leading to \"fake\" changes in pull requests.\n  Note that Visual Studio analyzers (IDE, StyleCop, etc.) may also recommend auto-refactoring, which could be applied implicitly.\n  To preserve the original EOL characters, manual code editing is necessary.\n  Therefore, \"fake\" changes result from auto-refactoring commands in IDEs like Visual Studio, Visual Code, Rider, etc.\n\n* **Our final recommendation** is to boot into Windows, use Visual Studio Community (which is free), refrain from using auto-refactoring commands, and ensure that EOLs remain unchanged.\n  If your OS differs, you **must** ensure that the appropriate settings are provided in the ``.gitattributes`` file to always commit files with ``CRLF`` EOL characters.\n"
  },
  {
    "path": "docs/building/releaseprocess.rst",
    "content": ".. _Gitflow Workflow: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow\r\n.. _GitHub Flow: https://docs.github.com/en/get-started/using-github/github-flow\r\n.. _develop: https://github.com/ThreeMammals/Ocelot/tree/develop\r\n.. _main: https://github.com/ThreeMammals/Ocelot/tree/main\r\n\r\nRelease Process\r\n===============\r\n\r\n* The *release process* is optimized when using Gitflow branching, as detailed here: `Gitflow Workflow`_.\r\n  It's important to note that the Ocelot team does not utilize `GitHub Flow`_, which, despite being quicker, does not align with the efficiency required for Ocelot's delivery.\r\n* Contributors are free to manage their pull requests and feature branches as they see fit to contribute to the '`develop`_' branch.\r\n* Maintainers have the autonomy to handle pull requests and merges. Any merges to the '`main`_' branch will trigger the release of packages to GitHub and NuGet.\r\n* In conclusion, while users should adhere to the guidelines in :doc:`../building/devprocess`, maintainers should follow the procedures outlined in :doc:`../building/releaseprocess`.\r\n\r\nStages\r\n------\r\n.. _Pair Programming: https://www.bing.com/search?q=Pair+Programming\r\n.. _SemVer: https://semver.org\r\n.. _GitVersion: https://gitversion.net/docs/\r\n.. _Ocelot NuGet packages: https://www.nuget.org/profiles/ThreeMammals\r\n.. _Release: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml\r\n.. _Environments: https://github.com/ThreeMammals/Ocelot/settings/environments\r\n.. _build.cake: https://github.com/ThreeMammals/Ocelot/blob/main/build.cake\r\n.. _ThreeMammals: https://github.com/ThreeMammals\r\n.. _milestone: https://github.com/ThreeMammals/Ocelot/milestones\r\n.. _releases: https://github.com/ThreeMammals/Ocelot/releases\r\n\r\nOcelot project follows this *release process* to incorporate work into NuGet packages:\r\n\r\n1. As a code reviewers, maintainers review pull requests and, if satisfactory, merge them; otherwise, they provide feedback for the contributor to address.\r\n   Contributors are supported through continuous `Pair Programming`_ sessions, which include multiple code reviews, resolving code review issues, and problem-solving.\r\n\r\n2. As a release engineers, maintainers must adhere to Semantic Versioning (`SemVer`_) supported by `GitVersion`_.\r\n   For breaking changes, maintainers should use the correct commit message (containing *\"+semver: breaking|major|minor|patch\"*) to ensure `GitVersion`_ applies the appropriate `SemVer`_ tags.\r\n   Manual tagging of the Ocelot repository should be avoided to prevent disruptions.\r\n\r\n3. Once a pull request is merged into the '`develop`_' branch, the `Ocelot NuGet packages`_ remain unchanged until a release is initiated.\r\n   When sufficient work warrants a new release, the '`develop`_' branch is merged into '`main`_' as a ``release/X.Y`` branch, triggering the `Release`_ workflow that builds the code, assigns versions, and pushes artifacts to GitHub and packages to NuGet.\r\n\r\n4. The release engineer, who holds the integration tokens in GitHub `Environments`_, automates each release build using the primary build script, '`build.cake`_'.\r\n   Automated or manual :doc:`../building/building` can be performed :ref:`b-in-terminal` or :ref:`b-with-ci-cd`.\r\n   The release engineer is also responsible for DevOps within the `ThreeMammals`_ organization, across all (sub)repositories, supporting the primary build script, and scripting for other repositories.\r\n\r\n5. The release engineer drafts the ``ReleaseNotes.md`` template file, informing the community about key aspects of the release, including new or updated features, bug fixes, documentation updates, breaking changes, contributor acknowledgments, version upgrade guidelines, and more.\r\n\r\n6. The final stage of the *release process* involves returning to GitHub to close the current `milestone`_, ensuring that:\r\n\r\n   * All issues within the `milestone`_ are closed; any remaining work from open issues should be transferred to the next `milestone`_.\r\n   * All pull requests associated with the `milestone`_ are either closed or reassigned to the upcoming release `milestone`_.\r\n   * Release Notes are published on GitHub `releases`_, with an additional review of the text.\r\n   * The published release is designated as the latest, provided the corresponding `Ocelot NuGet packages`_ have been successfully uploaded to the `ThreeMammals <https://www.nuget.org/profiles/ThreeMammals>`__ account.\r\n\r\n7. Optional support for the major version ``X.Y.0`` should be available in cases such as Microsoft official patches and critical Ocelot defects of that major version.\r\n   Maintainers should release patched versions ``X.Y.1-z`` as hot-fix patch versions.\r\n\r\nNotes\r\n-----\r\n.. _GitHub Actions: https://docs.github.com/en/actions\r\n.. _Actions: https://github.com/ThreeMammals/Ocelot/actions\r\n.. _Tom Pallister: https://github.com/TomPallister\r\n.. _Raman Maksimchuk: https://github.com/raman-m\r\n.. _Ocelot Team: https://github.com/orgs/ThreeMammals/teams\r\n\r\n**Note 1**: All NuGet package builds and releases are conducted through the `GitHub Actions`_ CI/CD provider.\r\nFor details, refer to the dedicated `Actions`_ dashboard, which should be used to monitor the current status of three workflows.\r\n\r\n**Note 2**: Currently, only `Tom Pallister`_, `Raman Maksimchuk`_, the owners—along with the `Ocelot Team`_ maintainers—have the authority to merge releases into the '`main`_' branch of the Ocelot repository.\r\nThis policy ensures that final :ref:`quality-gates` are in place.\r\nThe maintainers' primary focus during the final merge is to identify any security issues, as outlined in Stage 7 of the process.\r\n\r\n.. _quality-gates:\r\n\r\nQuality Gates\r\n-------------\r\n.. _code analysis rule set: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20%3CCodeAnalysisRuleSet%3E&type=code\r\n.. _codeanalysis.ruleset: https://github.com/ThreeMammals/Ocelot/blob/main/codeanalysis.ruleset\r\n.. _Overview of .NET source code analysis: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-9\r\n.. _StyleCop.Analyzers: https://www.nuget.org/packages/StyleCop.Analyzers\r\n.. _reference: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20StyleCop.Analyzers&type=code\r\n.. _here: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-options\r\n\r\n**Gate 1**: Static code analysis.\r\nThe Ocelot repository includes the following integrated style analyzers:\r\n\r\n* In-built IDE (.NET SDK):\r\n  The `code analysis rule set`_ is defined in the '`codeanalysis.ruleset`_' file, with configuration instructions available `here`_.\r\n  For comprehensive documentation, refer to the following article:\r\n\r\n  - Microsoft Learn: `Overview of .NET source code analysis`_\r\n\r\n* `StyleCop.Analyzers`_: The package is somewhat outdated with slow support, but Ocelot projects still `reference`_ it because it has remained functional since 2015/16 as an older style analyzer.\r\n  The Ocelot team plans to replace this library with a more advanced tool in upcoming releases.\r\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\r\n#\r\n# For the full list of built-in configuration values, see the documentation:\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\r\n\r\n# -- Project information -----------------------------------------------------\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\r\n\r\nproject = 'Ocelot Gateway'\r\ncopyright = ' 2016-2026 Three Mammals'\r\nauthor = 'Tom Gardham-Pallister, Raman Maksimchuk'\r\nrelease = '25.0 \".NET 10\"' # OK displayed\r\nversion = '25.0' # version is not displayed in either HTML pages or PDF docs\r\n\r\n# -- General configuration ---------------------------------------------------\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\r\n\r\nextensions = [\r\n    'sphinx_copybutton'\r\n]\r\n\r\ntemplates_path = ['_templates']\r\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\r\n\r\n# -- Options for HTML output -------------------------------------------------\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\r\n\r\n# HTML theming: https://www.sphinx-doc.org/en/master/usage/theming.html\r\n# HTML theme development: https://www.sphinx-doc.org/en/master/development/html_themes/index.html\r\n# https://alabaster.readthedocs.io/en/latest/\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_theme\r\nhtml_theme = 'alabaster'\r\n\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path\r\nhtml_static_path = ['_static']\r\nhtml_css_files = ['overrides.css']\r\n"
  },
  {
    "path": "docs/features/administration.rst",
    "content": ".. _IdentityServer: https://github.com/DuendeArchive/IdentityServer4\r\n.. _IdentityServer4: https://www.nuget.org/packages/IdentityServer4\r\n.. _Program: https://github.com/ThreeMammals/Ocelot.Administration.IdentityServer4/blob/main/sample/Program.cs\r\n.. _Ocelot.Administration.IdentityServer4: https://www.nuget.org/packages/Ocelot.Administration.IdentityServer4\r\n.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\r\n.. _Ocelot.postman_collection.json: https://github.com/ThreeMammals/Ocelot.Administration.IdentityServer4/blob/main/sample/Ocelot.postman_collection.json\r\n\r\nAdministration\r\n==============\r\n\r\n  **Ocelot extension package**: `Ocelot.Administration.IdentityServer4`_ with integrated `IdentityServer4`_ package by `IdentityServer org <https://github.com/IdentityServer>`_ (archived on March 6, 2025)\r\n\r\nOcelot supports changing configuration during runtime via an authenticated HTTP API.\r\nThis can be authenticated in two ways either using Ocelot's internal `IdentityServer`_ (for authenticating requests to the :ref:`administration-api` only) or hooking the :ref:`administration-api` authentication into your own `IdentityServer`_.\r\n\r\nThe first thing you need to do if you want to use the :ref:`administration-api` is bring in the relevant `Ocelot.Administration.IdentityServer4`_ package:\r\n\r\n.. code-block:: powershell\r\n\r\n  NuGet\\Install-Package Ocelot.Administration.IdentityServer4\r\n  dotnet add package Ocelot.Administration.IdentityServer4\r\n\r\nThis will bring down everything needed by the :ref:`administration-api`.\r\n\r\n  **Warning!** Currently, the *Administration* feature relies solely on the `IdentityServer4`_ package, whose `repository <https://github.com/DuendeArchive/IdentityServer4>`_ was archived by its owner on July 31, 2024 (for the first time) and again on March 6, 2025.\r\n  In release `24.0`_, the Ocelot team deprecated the `Ocelot.Administration.IdentityServer4`_ extension package.\r\n  However, `the repository <https://github.com/ThreeMammals/Ocelot.Administration.IdentityServer4>`_ remains available, allowing for potential patches.\r\n\r\n.. _ad-your-own-identityserver:\r\n\r\nYour Own IdentityServer [#f1]_\r\n------------------------------\r\n\r\nAll you need to do to hook into your own `IdentityServer`_ is add the following configuration options with authentication to your `Program`_.\r\nAfter that, we must pass these options to the ``AddAdministration()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f2]_, as shown below:\r\n\r\n.. code-block:: csharp\r\n\r\n    Action<JwtBearerOptions> options = o =>\r\n    {\r\n        o.Authority = \"https://identity-server-host:3333\";\r\n        o.RequireHttpsMetadata = true; // false in development environment\r\n        o.TokenValidationParameters = new()\r\n        {\r\n            ValidateAudience = false,\r\n        };\r\n        //...\r\n    };\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddAdministration(\"/administration\", options);\r\n\r\nYou now need to get a token from your `IdentityServer`_ and use in subsequent requests to Ocelot's :ref:`administration-api`.\r\n\r\n  **Note**: This feature is useful because the `IdentityServer`_ authentication middleware needs the URL of the server.\r\n  If you are using the :ref:`ad-internal-identityserver`, it might not always be possible to have the Ocelot URL.\r\n\r\n.. _ad-internal-identityserver:\r\n\r\nInternal IdentityServer\r\n-----------------------\r\n\r\nThe API is authenticated using Bearer tokens that you request from Ocelot itself.\r\nThis is provided by the amazing `IdentityServer`_ project that the .NET community has been using for several years.\r\nCheck it out.\r\n\r\nIn order to enable the administration section, you need to do a few things. First of all, add this to your initial `Program`_.\r\n\r\nThe path can be anything you want and it is obviously recommended don't use a URL you would like to route through with Ocelot as this will not work.\r\nThe administration uses the ``MapWhen`` functionality of ASP.NET Core and all requests to ``{root}/administration`` will be sent there not to the Ocelot middleware.\r\n\r\nThe secret is the client secret that Ocelot's internal `IdentityServer`_ will use to authenticate requests to the :ref:`administration-api`.\r\nThis can be whatever you want it to be!\r\nIn order to pass this secret string as parameter, we must call the ``AddAdministration()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f2]_, as shown below:\r\n\r\n.. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddAdministration(\"/administration\", \"secret\");\r\n\r\nIn order for the :ref:`administration-api` to work, Ocelot and `IdentityServer`_ must be able to call themselves for validation.\r\nThis means that you need to add the base URL of Ocelot to the global configuration if it is not the default ``http://localhost:5000``.\r\n\r\n  **Note**: If you are using something like Docker to host Ocelot, it might not be able to call back to ``localhost``, etc., and you need to know what you are doing with Docker networking in this scenario.\r\n\r\nConfiguration can be done as follows:\r\n\r\n* If you want to run on a different host and port locally:\r\n\r\n  .. code-block:: json\r\n\r\n      \"GlobalConfiguration\": {\r\n        \"BaseUrl\": \"http://localhost:5580\"\r\n      }\r\n\r\n* or if Ocelot is exposed via DNS:\r\n\r\n  .. code-block:: json\r\n\r\n      \"GlobalConfiguration\": {\r\n        \"BaseUrl\": \"http://mydns.net\"\r\n      }\r\n\r\nNow, if you went with the configuration options above and want to access the API, you can use the Postman scripts called `Ocelot.postman_collection.json`_ in the solution to change the Ocelot configuration. \r\nObviously these will need to be changed if you are running Ocelot on a different URL to ``http://localhost:5000``.\r\n\r\nThe scripts show you how to request a Bearer token from Ocelot and then use it to GET the existing configuration and POST a configuration.\r\n\r\nIf you are running multiple Ocelot instances in a cluster then you need to use a certificate to sign the Bearer tokens used to access the :ref:`administration-api`.\r\n\r\nIn order to do this, you need to add two more environmental variables for each Ocelot in the cluster:\r\n\r\n1. ``OCELOT_CERTIFICATE``: The path to a certificate that can be used to sign the tokens. The certificate needs to be of the type X509 and obviously Ocelot needs to be able to access it.\r\n2. ``OCELOT_CERTIFICATE_PASSWORD``: The password for the certificate.\r\n\r\nNormally Ocelot just uses temporary signing credentials but if you set these environmental variables then it will use the certificate. \r\nIf all the other Ocelot instances in the cluster have the same certificate then you are good!\r\n\r\n.. _administration-api:\r\n\r\nAdministration API\r\n------------------\r\n\r\n* **POST** ``{adminPath}/connect/token``\r\n\r\n    This gets a token for use with the admin area using the client credentials we talk about setting above.\r\n    Under the hood this calls into an `IdentityServer`_ hosted within Ocelot.\r\n\r\n    The body of the request is form-data as follows:\r\n\r\n    * ``client_id`` set as admin\r\n    * ``client_secret`` set as whatever you used when setting up the administration services.\r\n    * ``scope`` set as admin\r\n    * ``grant_type`` set as client_credentials\r\n\r\n* **GET** ``{adminPath}/configuration``\r\n\r\n    This gets the current Ocelot configuration. It is exactly the same JSON we use to set Ocelot up with in the first place.\r\n\r\n* **POST** ``{adminPath}/configuration``\r\n\r\n    This overwrites the existing configuration (should probably be a PUT!).\r\n    We recommend getting your config from the GET endpoint, making any changes and posting it back... simples.\r\n\r\n    The body of the request is JSON and it is the same format as the `FileConfiguration <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileConfiguration.cs>`_\r\n    that we use to set up Ocelot on a file system. \r\n\r\n    Please note, if you want to use this API then the process running Ocelot must have permission to write to the disk where your ``ocelot.json`` or ``ocelot.{environment}.json`` is located.\r\n    This is because Ocelot will overwrite them on save. \r\n\r\n* **DELETE** ``{adminPath}/outputcache/{region}``\r\n\r\n    This clears a region of the cache. If you are using a backplane, it will clear all instances of the cache!\r\n    Giving your the ability to run a cluster of Ocelots and cache over all of them in memory and clear them all at the same time, so just use a distributed cache.\r\n\r\n    The region is whatever you set against the ``Region`` field in the `FileCacheOptions <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FileCacheOptions&type=code>`_ section of the Ocelot configuration.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The \":ref:`Your Own IdentityServer <ad-your-own-identityserver>`\" feature was implemented for issue `228 <https://github.com/ThreeMammals/Ocelot/issues/228>`_.\r\n.. [#f2] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\r\n"
  },
  {
    "path": "docs/features/aggregation.rst",
    "content": "Aggregation\n===========\n\n*Aggregation*, also known as HTTP response data aggregation, is a well-known Backend for Frontend pattern of Microservices architecture.\n\n  * `Backend for Frontend (BFF) Pattern: Microservices for UX | Teleport Academy <https://goteleport.com/learn/backend-for-frontend-bff-pattern/>`_\n  * `Gateway Aggregation pattern | Azure Architecture Center | Microsoft Learn <https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation>`_\n  * `Backends for Frontends pattern | Azure Architecture Center | Microsoft Learn <https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends>`_\n  * `Implement API Gateways with Ocelot | .NET microservices - Architecture e-book | Microsoft Learn <https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/implement-api-gateways-with-ocelot>`_\n\nOcelot allows you to specify *Aggregate Routes* [#f1]_ that combine multiple normal routes and map their responses into a single object.\nThis is particularly useful when a client is making multiple requests to a server that could be consolidated into one.\nThis feature supports the implementation of a Backend for Frontend (BFF) architecture using Ocelot.\n\nConfiguration\n-------------\n\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\n\nIn order to set this up, you need to configure the `ocelot.json`_ file as follows.\nIn this example, two normal routes are specified, each having a ``Key`` property.\nAn *aggregation* is then defined, which combines the two routes using their keys listed in ``RouteKeys``, and the ``UpstreamPathTemplate`` is set up to function like a normal route.\n\n  Note that duplicate ``UpstreamPathTemplates`` are not allowed between ``Routes`` and ``Aggregates``.\n  You can use all of Ocelot's normal route options, except for ``RequestIdKey``, as explained in the :ref:`agg-gotchas` section.\n\n.. code-block:: json\n  :emphasize-lines: 11, 21, 24\n\n  {\n    \"Routes\": [\n      {\n        \"UpstreamHttpMethod\": [ \"Get\" ],\n        \"UpstreamPathTemplate\": \"/laura\",\n        \"DownstreamPathTemplate\": \"/\",\n        \"DownstreamScheme\": \"http\",\n        \"DownstreamHostAndPorts\": [\n          { \"Host\": \"localhost\", \"Port\": 51881 }\n        ],\n        \"Key\": \"Laura\"\n      },\n      {\n        \"UpstreamHttpMethod\": [ \"Get\" ],\n        \"UpstreamPathTemplate\": \"/tom\",\n        \"DownstreamPathTemplate\": \"/\",\n        \"DownstreamScheme\": \"http\",\n        \"DownstreamHostAndPorts\": [\n          { \"Host\": \"localhost\", \"Port\": 51882 }\n        ],\n        \"Key\": \"Tom\"\n      }\n    ],\n    \"Aggregates\": [\n      {\n        \"UpstreamPathTemplate\": \"/\",\n        \"RouteKeys\": [ \"Tom\", \"Laura\" ]\n      }\n    ]\n  }\n\nYou can also set ``UpstreamHost`` and ``RouteIsCaseSensitive`` in the *aggregation* configuration. These settings behave the same as in other routes.\n\nIf the route ``/tom`` returned a body of ``{\"Age\": 19}`` and ``/laura`` returned ``{\"Age\": 25}``, the response after *aggregation* would be as follows:\n\n.. code-block:: json\n\n    {\"Tom\":{\"Age\": 19},\"Laura\":{\"Age\": 25}}\n\nAt the moment, the *aggregation* is quite simple.\nOcelot retrieves the response from your downstream service and inserts it into a JSON dictionary, as shown above.\nThe route ``Key`` becomes the key of the dictionary, and the response body from your downstream service serves as the value.\nThe resulting object is plain JSON without any formatting or additional spaces.\n\n  **Note 1**: All headers will be lost from the downstream service's response.\n\n  **Note 2**: Ocelot will always return the content type ``application/json`` for an aggregate request.\n\n  **Note 3**: If your downstream services return a ``404`` `Not Found <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404>`_, the aggregate will simply return nothing for that downstream service.\n  It will not change the aggregate response to a ``404``, even if all the downstream services return a ``404``.\n\n.. _agg-complex-aggregation:\n\nComplex Aggregation [#f2]_\n--------------------------\n\nImagine you would like to use aggregated queries but don't have all the parameters for your queries.\nFirst, you need to call an endpoint to obtain the necessary data, such as a user's ID, and then return the user's details.\n\nLet's say we have an endpoint that returns a series of comments referencing various users or threads.\nThe author of the comments is identified by their ID, but you want to return all the details about the author.\n\nHere, you could use aggregation to: 1) retrieve all the comments, and 2) attach the author details.\nIn fact, two endpoints are called, but for the second, you dynamically replace the user's ID in the route to obtain the details.\n\nIn concrete terms:\n\n1) ``/Comments`` contains the ``authorId`` property.\n2) ``/users/{userId}``, with ``{userId}`` replaced by ``authorId``, is used to obtain the user's details.\n\nTo perform the mapping, you need to use the ``RouteKeysConfig`` list of configuration options for aggreagte route, typed as ``AggregateRouteConfig`` class:\n\n.. code-block:: json\n\n  \"RouteKeysConfig\": [\n    {\n      \"RouteKey\": \"UserDetails\",\n      \"JsonPath\": \"$[*].authorId\",\n      \"Parameter\": \"userId\"\n    }\n  ]\n\n``RouteKey`` is used as a reference for the route, ``JsonPath`` indicates where the parameter of interest is located in the first request's response body, and ``Parameter`` specifies that the value for ``authorId`` should be used as the request parameter ``userId``.\n\nThe final configuration is as follows:\n\n.. code-block:: json\n  :emphasize-lines: 27-30\n\n  {\n    \"Routes\": [\n      {\n        \"UpstreamPathTemplate\": \"/Comments\",\n        \"DownstreamPathTemplate\": \"/\",\n        // ...\n        \"Key\": \"Comments\"\n      },\n      {\n        \"UpstreamPathTemplate\": \"/UserDetails/{userId}\",\n        \"DownstreamPathTemplate\": \"/users/{userId}\",\n        // ...\n        \"Key\": \"UserDetails\"\n      },\n      {\n        \"UpstreamPathTemplate\": \"/PostDetails/{postId}\",\n        \"DownstreamPathTemplate\": \"/posts/{postId}\",\n        // ...\n        \"Key\": \"PostDetails\"\n      }\n    ],\n    \"Aggregates\": [\n      {\n        \"UpstreamPathTemplate\": \"/\",\n        \"UpstreamHost\": \"localhost\",\n        \"RouteKeys\": [ \"Comments\", \"UserDetails\", \"PostDetails\" ],\n        \"RouteKeysConfig\": [\n          { \"RouteKey\": \"UserDetails\", \"JsonPath\": \"$[*].writerId\", \"Parameter\": \"userId\" },\n          { \"RouteKey\": \"PostDetails\", \"JsonPath\": \"$[*].postId\", \"Parameter\": \"postId\" }\n        ]\n      }\n    ]\n  }\n\nCustom Aggregators\n------------------\n\nOcelot started with basic request *aggregation*, and since then, a more advanced method has been added.\nThis method allows the user to take the responses from downstream services and aggregate them into a response object.\nThe `ocelot.json`_ setup is almost identical to the basic *aggregation* approach, except that you need to add an ``Aggregator`` property, as shown below:\n\n.. code-block:: json\n  :emphasize-lines: 20\n\n  {\n    \"Routes\": [\n      {\n        \"UpstreamPathTemplate\": \"/laura\",\n        \"DownstreamPathTemplate\": \"/\",\n        // ...\n        \"Key\": \"Laura\"\n      },\n      {\n        \"UpstreamPathTemplate\": \"/tom\",\n        \"DownstreamPathTemplate\": \"/\",\n        // ...\n        \"Key\": \"Tom\"\n      }\n    ],\n    \"Aggregates\": [ \n      {\n        \"UpstreamPathTemplate\": \"/\",\n        \"RouteKeys\": [ \"Tom\", \"Laura\" ],\n        \"Aggregator\": \"MyAggregator\"\n      }\n    ]\n  }\n\nHere, we have added an aggregator called ``MyAggregator``. Ocelot will look for this aggregator when it tries to aggregate this route.\n\nIn order to make the aggregator available in Ocelot Core, we must add the ``MyAggregator`` to the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f3]_, as shown below:\n\n.. code-block:: csharp\n  :emphasize-lines: 5\n\n  using Ocelot.Multiplexer;\n\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddSingletonDefinedAggregator<MyAggregator>();\n\nNow, when Ocelot tries to aggregate the route above, it will find the ``MyAggregator`` in the DI-container and use it to aggregate the route.\nSince the ``MyAggregator`` is registered in the DI-container, you can add any dependencies it needs to the container, as shown below:\n    \n.. code-block:: csharp\n  :emphasize-lines: 2, 6\n\n  builder.Services\n      .AddSingleton<MyDependency>();\n  // ...\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddSingletonDefinedAggregator<MyAggregator>();\n\nIn this example, ``MyAggregator`` depends on ``MyDependency``, and it will be resolved by the DI container.\nIn addition to this, Ocelot lets you add transient aggregators, as shown below:\n\n.. code-block:: csharp\n  :emphasize-lines: 3\n\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddTransientDefinedAggregator<MyAggregator>();\n\nIn order to create an *aggregator*, you must implement the following interface:\n\n.. code-block:: csharp\n\n    public interface IDefinedAggregator\n    {\n        Task<DownstreamResponse> Aggregate(List<HttpContext> responses);\n    }\n\nWith this feature, you can essentially do whatever you want, as the ``HttpContext`` objects contain the results of all the aggregate requests.\n\n  Please note that if the ``HttpClient`` throws an exception when making a request to a route in the aggregate, you will not receive a ``HttpContext`` for it.\n  However, you will receive one for any that succeed. If an exception is thrown, it will be logged.\n\nBelow is an example of an *aggregator* that can be implemented for your solution:\n\n.. code-block:: csharp\n\n  public class MyAggregator : IDefinedAggregator\n  {\n      public async Task<DownstreamResponse> Aggregate(List<HttpContext> responseHttpContexts)\n      {\n          // The aggregator gets a list of downstream responses as parameter.\n          // You can now implement your own logic to aggregate the responses (including bodies and headers) from the downstream services\n          var responses = responseHttpContexts.Select(x => x.Items.DownstreamResponse()).ToArray();\n\n          // In this example we are concatenating the results,\n          // but you could create a more complex construct, up to you.\n          var contentList = new List<string>();\n          foreach (var response in responses)\n          {\n              var content = await response.Content.ReadAsStringAsync();\n              contentList.Add(content);\n          }\n\n          // The only constraint here: You must return a DownstreamResponse object.\n          return new DownstreamResponse(\n              new StringContent(JsonConvert.SerializeObject(contentList)),\n              HttpStatusCode.OK,\n              responses.SelectMany(x => x.Headers).ToList(),\n              \"reason\");\n      }\n  }\n\n.. _agg-gotchas:\n\nGotchas\n-------\n\n* You cannot use routes with specific ``RequestIdKeys``, as this would be overly complicated to track.\n* *Aggregation* supports only the ``GET`` HTTP verb.\n* *Aggregation* allows the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data.\n  Form data and attached files should also be forwarded.\n  It is essential to specify the ``Content-Length`` header in requests to the upstream; otherwise, Ocelot will log warnings such as: *\"Aggregation does not support body copy without a Content-Length header!\"*\n\n\"\"\"\"\n\n.. [#f1] This feature was requested as part of issue `79`_, and further improvements were made as part of issue `298`_. A significant refactoring and revision of the `Multiplexer <https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/Multiplexer>`_ design was carried out on March 4, 2024, in version `23.1`_. See pull requests `1462`_ and `1826`_ for more details.\n.. [#f2] The \":ref:`Complex Aggregation <agg-complex-aggregation>`\" feature is still in its early stages, but it enables searching for data based on an initial request. This feature was requested as part of issue `661`_, introduced in pull request `704`_, and released in version `13.4`_.\n.. [#f3] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\n\n.. _79: https://github.com/ThreeMammals/Ocelot/issues/79\n.. _298: https://github.com/ThreeMammals/Ocelot/issues/298\n.. _661: https://github.com/ThreeMammals/Ocelot/issues/661\n\n.. _704: https://github.com/ThreeMammals/Ocelot/pull/704\n.. _1462: https://github.com/ThreeMammals/Ocelot/pull/1462\n.. _1826: https://github.com/ThreeMammals/Ocelot/pull/1826\n\n.. _13.4: https://github.com/ThreeMammals/Ocelot/releases/tag/13.4.1\n.. _23.1: https://github.com/ThreeMammals/Ocelot/releases/tag/23.1.0\n"
  },
  {
    "path": "docs/features/authentication.rst",
    "content": ".. _scheme: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n\r\nAuthentication\r\n==============\r\n\r\nIn order to authenticate routes and subsequently use any of Ocelot's claims based features such as authorization or modifying the request with values from the token,\r\nusers must register authentication services in their `Program`_ as usual but they provide a `scheme`_ \r\n(authentication provider key) with each registration e.g.\r\n\r\n.. code-block:: csharp\r\n\r\n    const string AuthenticationProviderKey = \"MyKey\"; // aka scheme\r\n    builder.Services\r\n        .AddAuthentication()\r\n        .AddJwtBearer(AuthenticationProviderKey, options =>\r\n        {\r\n            // authentication setup via options initialization\r\n        });\r\n\r\nIn this example, ``MyKey`` is the `scheme`_ with which this provider has been registered, but for JWT bearer authentication, the scheme is usually ``Bearer``.\r\nWe then map this to a route in the configuration using the following :ref:`authentication-options-schema` options:\r\n\r\n* ``AuthenticationProviderKey`` is a string, the legacy definition of :ref:`Single Authentication Scheme <authentication-scheme>`.\r\n* ``AuthenticationProviderKeys`` is an array of strings, the recommended definition of :ref:`Multiple Authentication Schemes <authentication-multiple>` feature.\r\n\r\n.. _authentication-options-schema:\r\n\r\n``AuthenticationOptions`` Schema\r\n--------------------------------\r\n\r\n.. _FileAuthenticationOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs\r\n\r\n  Class: `FileAuthenticationOptions`_\r\n\r\nThe following is the full *authentication* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`.\r\nNot all of these options need to be configured; however, the ``AuthenticationProviderKeys`` option is mandatory when ``AuthenticationProviderKey`` is absent.\r\n\r\n.. code-block:: json\r\n\r\n  \"AuthenticationOptions\": {\r\n    \"AllowAnonymous\": false, // nullable boolean\r\n    \"AllowedScopes\": [], // array of strings\r\n    \"AuthenticationProviderKey\": \"\", // deprecated! -> use AuthenticationProviderKeys\r\n    \"AuthenticationProviderKeys\": [] // array of strings\r\n  }\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - *Option*\r\n    - *Description*\r\n  * - ``AllowAnonymous``\r\n    - Excludes a route from global *authentication options* by setting it to ``true``.\r\n      If the global option disables authentication by forcibly having a ``true`` value, then at the route level the option can include a route to be authenticated by setting it to ``false``.\r\n      For more details, refer to the \":ref:`Configuration and AllowAnonymous <authentication-configuration>`\" section.\r\n  * - ``AllowedScopes``\r\n    - If specified, enables authorization based on the ``scope`` claim after successful authentication by a configured authentication provider.\r\n      For more details, refer to the \":ref:`authentication-allowed-scopes`\" section.\r\n  * - ``AuthenticationProviderKey``\r\n    - Maps a configured authentication provider, identified by a key (scheme), to a route that requires authentication.\r\n      *Note: This option is deprecated—see the warning below.*\r\n      For more details, refer to the \":ref:`Single Authentication Scheme <authentication-scheme>`\" section.\r\n  * - ``AuthenticationProviderKeys``\r\n    - Maps all configured authentication providers, identified by their schemes, to a route that requires authentication.\r\n      For more details, refer to the \":ref:`Multiple Authentication Schemes <authentication-multiple>`\" section.\r\n\r\n.. warning::\r\n  The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_! Use the ``AuthenticationProviderKeys`` array option instead.\r\n  Note that ``AuthenticationProviderKey`` will be removed in version `25.0`_.\r\n  For backward compatibility in version `24.1`_, the ``AuthenticationProviderKey`` option takes precedence over the schemes in the ``AuthenticationProviderKeys`` array.\r\n  If the ``AuthenticationProviderKey`` scheme provider fails, the remaining schemes in the ``AuthenticationProviderKeys`` array will enforce the appropriate authentication providers in the specified order.\r\n\r\n.. _authentication-scheme:\r\n\r\nSingle Authentication Scheme [#f1]_\r\n-----------------------------------\r\n\r\n  Option: ``AuthenticationProviderKey``\r\n\r\nWe map authentication provider to a Route in the configuration e.g.\r\n\r\n.. code-block:: json\r\n\r\n  \"AuthenticationOptions\": {\r\n    \"AuthenticationProviderKey\": \"MyKey\",\r\n    \"AllowedScopes\": []\r\n  }\r\n\r\nWhen Ocelot runs it will look at this routes ``AuthenticationProviderKey`` and check that there is an authentication provider registered with the given key.\r\nIf there isn't then Ocelot will not start up. If there is then the route will use that provider when it executes.\r\n\r\nIf a route is authenticated, Ocelot will invoke whatever scheme is associated with it while executing the authentication middleware.\r\nIf the request fails authentication, Ocelot returns a HTTP status code `401 Unauthorized <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401>`_.\r\n\r\n.. _authentication-multiple:\r\n\r\nMultiple Authentication Schemes [#f2]_\r\n--------------------------------------\r\n.. _multiple authentication schemes: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme#use-multiple-authentication-schemes\r\n\r\n  Option: ``AuthenticationProviderKeys``\r\n\r\nIn the real world of ASP.NET Core, apps may need to support multiple types of authentication by a single Ocelot app instance.\r\nTo register `multiple authentication schemes`_ (`authentication provider keys <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20AuthenticationProviderKey&type=code>`_) for each appropriate authentication provider,\r\nuse and develop this abstract configuration of two or more schemes:\r\n\r\n.. code-block:: csharp\r\n\r\n    var DefaultScheme = JwtBearerDefaults.AuthenticationScheme; // Bearer\r\n    builder.Services\r\n        .AddAuthentication()\r\n        .AddJwtBearer(DefaultScheme, options => { /* JWT setup */ })\r\n        // AddJwtBearer, AddCookie, AddIdentityServerAuthentication etc. \r\n        .AddMyProvider(\"MyKey\", options => { /* Custom auth setup */ });\r\n\r\nIn this example, the ``MyKey`` and ``Bearer`` schemes represent the keys with which these providers were registered.\r\nWe then map these schemes to a route in the configuration as shown below.\r\n\r\n.. code-block:: json\r\n\r\n  \"AuthenticationOptions\": {\r\n    \"AuthenticationProviderKeys\": [ \"Bearer\", \"MyKey\" ] // The order matters!\r\n    \"AllowedScopes\": []\r\n  }\r\n\r\nAfterward, Ocelot applies all steps that are specified for ``AuthenticationProviderKey`` as :ref:`Single Authentication Scheme <authentication-scheme>`.\r\nThe order of the keys in an array definition does matter! We use a \"First One Wins\" authentication strategy.\r\n\r\n.. _authentication-configuration:\r\n\r\nConfiguration and ``AllowAnonymous`` [#f3]_\r\n-------------------------------------------\r\n\r\nTo configure *authentication options* uniformly across all static routes, define them in ``GlobalConfiguration`` section using the :ref:`authentication-options-schema`.\r\nIf *authentication options* are specified in both ``GlobalConfiguration`` and a route (i.e., ``AuthenticationProviderKey`` or ``AuthenticationProviderKeys`` are set), the route-level configuration takes precedence.\r\n\r\nExcluding a route from global *authentication options* is possible by setting ``AllowAnonymous`` option to ``true``.\r\nThis prevents the route from requiring authentication, keeping it open and anonymous.\r\n\r\nIn the following example:\r\n\r\n* The first route is authenticated using the ``MyGlobalKey`` provider's scheme.\r\n* The second route uses the ``MyKey`` provider's scheme.\r\n* The third route is not authenticated.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 4, 8-11, 15-17, 22-26\r\n\r\n  \"Routes\": [\r\n    {\r\n      // route #1 props...\r\n      \"AuthenticationOptions\": {}\r\n    },\r\n    {\r\n      // route #2 props...\r\n      \"AuthenticationOptions\": {\r\n        \"AuthenticationProviderKeys\": [ \"MyKey\" ],\r\n        \"AllowedScopes\": [ \"Bob\" ]\r\n      }\r\n    },\r\n    {\r\n      // route #3 props...\r\n      \"AuthenticationOptions\": {\r\n        \"AllowAnonymous\": true\r\n      }\r\n    }\r\n  ],\r\n  \"GlobalConfiguration\": {\r\n    \"BaseUrl\": \"http://ocelot.net\",\r\n    \"AuthenticationOptions\": {\r\n      \"RouteKeys\": [], // empty -> no grouping, thus opts will apply to all routes\r\n      \"AuthenticationProviderKeys\": [ \"MyGlobalKey\" ],\r\n      \"AllowedScopes\": [ \"Admin\" ]\r\n    }\r\n  }\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note**: Ocelot performs a per-option merging algorithm to combine route and global ``AuthenticationOptions``.\r\n  If global ``AuthenticationProviderKeys`` are defined together with global ``AllowedScopes``, then route options should be specified as a pair of scheme and scopes; otherwise, a scope should not belong to the global authentication provider.\r\n  Moreover, the route scopes array entirely overrides the global scopes array, so the two collections are not merged but rather interchangeable.\r\n\r\n.. _authentication-global-configuration:\r\n\r\nGlobal Configuration [#f4]_\r\n---------------------------\r\n\r\nSince the global configuration for static routes has already been described above, here are additional details regarding dynamic routes, whose configuration was not supported in versions prior to `24.1`_.\r\nStarting with version `24.1`_, global and route *authentication options* for :ref:`Dynamic Routing <routing-dynamic>` were introduced.\r\nThese global options may also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 6-9, 18-22\r\n\r\n  {\r\n    \"DynamicRoutes\": [\r\n      {\r\n        \"Key\": \"R1\", // optional\r\n        \"ServiceName\": \"my-service\",\r\n        \"AuthenticationOptions\": {\r\n          \"AuthenticationProviderKeys\": [\"MyKey\"], // custom authentication provider\r\n          \"AllowedScopes\": [\"my-service\"] // require authorization with a 'scope' claim set to the value 'my-service'\r\n        }\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://ocelot.net\",\r\n      \"DownstreamScheme\": \"http\",\r\n      \"ServiceDiscoveryProvider\": {\r\n        // required section for dynamic routing\r\n      },\r\n      \"AuthenticationOptions\": {\r\n        \"RouteKeys\": [], // or null, no grouping, thus opts apply to all dynamic routes\r\n        \"AuthenticationProviderKeys\": [\"Bearer\"], // use a global JWT bearer auth provider for all discovered services\r\n        \"AllowedScopes\": [\"oc-admin\"] // require the global 'scope' claim to gain access to all discovered services\r\n      }\r\n    }\r\n  }\r\n\r\nIn this configuration, an ``oc-admin`` scope authorization is applied to all implicit dynamic routes by the global ``Bearer`` JWT signing service.\r\nHowever, for the “my-service” service, authorization with the ``my-service`` scope is applied, and authentication is provided by another source of tokens named ``MyKey``.\r\n\r\n.. note::\r\n\r\n  1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``AuthenticationOptions``, the global options will apply to all routes.\r\n  If the array contains route keys, it defines a single group of routes to which the global options apply.\r\n  Routes excluded from this group must specify their own route-level ``AuthenticationOptions``.\r\n\r\n  2. Prior to version `24.1`_, global and dynamic route ``AuthenticationOptions`` were not available.\r\n  Starting with version `24.1`_, global configuration is supported for both static and dynamic routes.\r\n\r\n.. _authentication-allowed-scopes:\r\n\r\nAllowed Scopes\r\n--------------\r\n\r\n  | Option: ``AllowedScopes``\r\n  | Middleware: :ref:`authorization-middleware`\r\n\r\nTo set up authorization by scopes from the ``AllowedScopes`` collection, after successful authentication by the middleware and after claims have been transformed,\r\nthe authorization middleware in Ocelot retrieves all user claims (from the token) of the '``scope``' type and ensures that the user has at least one of the scopes in the list.\r\nThis provides a way to restrict access to a route on a per-scope basis.\r\n\r\n.. note::\r\n  [#f5]_ Depending on the authentication provider, incoming tokens embed the '``scope``' claim value in the body either as an array or as a single space-separated string of multiple values.\r\n  For instance, :ref:`authentication-identity-server` use an array, whereas most :ref:`authentication-jwt-tokens` providers generate a space-separated list of scopes, in accordance with `RFC 8693`_, as stated in section \"`4.2. \"scope\" (Scopes) Claim`_\".\r\n  Since version `24.1`_, Ocelot supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the ``scope`` claim in the ``ScopesAuthorizer`` service, also known as the ``IScopesAuthorizer`` service in the DI container.\r\n\r\n.. note::\r\n  Starting with version `24.1`_, specifying global *allowed scopes* is exclusively supported.\r\n  Be cautious when overriding the global ``AllowedScopes`` array with a route-level ``AllowedScopes`` array;\r\n  a combination of the route scheme (``AuthenticationProviderKeys`` array) and its *allowed scopes* might be required, since new *allowed scopes* could belong to another authentication provider's security model.\r\n  For more details, refer to the \":ref:`Configuration and AllowAnonymous <authentication-configuration>`\" and \":ref:`Global Configuration <authentication-global-configuration>`\" sections.\r\n\r\n.. _authentication-jwt-tokens:\r\n\r\nJWT Tokens\r\n----------\r\n\r\nIf you want to authenticate using JWT tokens maybe from a provider like `Auth0 <https://auth0.com/>`_, you can register your authentication middleware as normal e.g.\r\n\r\n.. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddAuthentication()\r\n        .AddJwtBearer(\"Auth0\", options =>\r\n        {\r\n            options.Authority = \"test\";\r\n            options.Audience = \"test\";\r\n        });\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\nThen map the authentication provider key to a route in your configuration e.g.\r\n\r\n.. code-block:: json\r\n\r\n  \"AuthenticationOptions\": {\r\n    \"AuthenticationProviderKeys\": [\"Auth0\"],\r\n  }\r\n\r\n**JWT Tokens Docs**\r\n\r\n    * Microsoft Learn: `Authentication and authorization in minimal APIs <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security>`_\r\n    * Andrew Lock | .NET Escapades: `A look behind the JWT bearer authentication middleware in ASP.NET Core <https://andrewlock.net/a-look-behind-the-jwt-bearer-authentication-middleware-in-asp-net-core/>`_\r\n\r\n.. _authentication-identity-server:\r\n\r\nIdentity Server Bearer Tokens\r\n-----------------------------\r\n\r\nIn order to use `IdentityServer <https://github.com/IdentityServer>`_ bearer tokens, register your IdentityServer services as usual in `Program`_ with a `scheme`_ (key).\r\nIf you don't understand how to do this, please consult the IdentityServer `documentation <https://identityserver4.readthedocs.io/>`_.\r\n\r\n.. code-block:: csharp\r\n\r\n    Action<JwtBearerOptions> options = o =>\r\n    {\r\n        o.Authority = \"https://whereyouridentityserverlives.com\";\r\n        // ...\r\n    };\r\n    builder.Services\r\n        .AddAuthentication()\r\n        .AddJwtBearer(\"IS4\", options);\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\nThen map the authentication provider key to a route in your configuration e.g.\r\n\r\n.. code-block:: json\r\n\r\n  \"AuthenticationOptions\": {\r\n    \"AuthenticationProviderKeys\": [\"IS4\"],\r\n  }\r\n\r\nAuth0 by Okta\r\n-------------\r\n\r\nYet another identity provider by `Okta <https://www.okta.com/>`_, see `Auth0 Developer Resources <https://developer.auth0.com/>`_.\r\n\r\nAdd the following, at minimum, to your startup `Program`_:\r\n\r\n.. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddAuthentication()\r\n        .AddJwtBearer(\"Okta\", o =>\r\n        {\r\n            var conf = builder.Configuration;\r\n            o.Audience = conf[\"Authentication:Okta:Audience\"]; // Okta Authorization server Audience\r\n            o.Authority = conf[\"Authentication:Okta:Server\"]; // Okta Authorization Issuer URI URL e.g. https://{subdomain}.okta.com/oauth2/{authidentifier}\r\n        });\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\n    var app = builder.Build();\r\n    await app\r\n        .UseAuthentication()\r\n        .UseOcelot();\r\n    await app.RunAsync();\r\n\r\nIn order to get Ocelot to view the scope claim from Okta properly, you have to add the following to map the default Okta ``scp`` claim to ``scope``:\r\n\r\n.. code-block:: csharp\r\n\r\n    // Map Okta \"scp\" to \"scope\" claims instead of http://schemas.microsoft.com/identity/claims/scope to allow Ocelot to read/verify them\r\n    JsonWebTokenHandler.DefaultInboundClaimTypeMap.Remove(\"scp\");\r\n    JsonWebTokenHandler.DefaultInboundClaimTypeMap.Add(\"scp\", \"scope\");\r\n\r\n**Okta Notes**\r\n\r\n    1. Issue `446`_ contains some code and examples that might help with Okta integration.\r\n    2. Here is documentation for better clarity on claims mapping: `Mapping, customizing, and transforming claims in ASP.NET Core`_.\r\n    3. It is highly advisable to read and understand the :ref:`authentication-warnings` related to the critical changes in authentication when utilizing .NET 8.\r\n\r\n.. _authentication-warnings:\r\n\r\nWarnings\r\n--------\r\n\r\n.. warning::\r\n  .NET 8 introduced a breaking change where ``JwtSecurityToken`` was replaced with ``JsonWebToken`` to enhance performance and reliability.\r\n  Consequently, their handlers were changed ``JwtSecurityTokenHandler`` to ``JsonWebTokenHandler``.\r\n  For a complete understanding of .NET 8 breaking change related to JWT tokens, please refer to the Microsoft Learn documentation: \"`Security token events return a JsonWebToken <https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events>`__\".\r\n\r\nLinks\r\n-----\r\n.. _Mapping, customizing, and transforming claims in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-9.0\r\n\r\n* Microsoft Learn: `Overview of ASP.NET Core authentication <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/>`_\r\n* Microsoft Learn: `Authorize with a specific scheme in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme>`_\r\n* Microsoft Learn: `Policy schemes in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes>`_\r\n* Microsoft Learn: `Mapping, customizing, and transforming claims in ASP.NET Core`_\r\n* Microsoft .NET Blog: `ASP.NET Core Authentication with IdentityServer4 <https://devblogs.microsoft.com/dotnet/asp-net-core-authentication-with-identityserver4/>`_\r\n\r\nRoadmap\r\n-------\r\n\r\nNothing is currently in the stack, but the Ocelot team is rethinking a new version of the \":doc:`../features/administration`\" feature, which is closely dependent on authentication.\r\n\r\nWe invite you to add more examples if you have integrated with other identity providers and the integration solution is working.\r\nPlease open a \"`Show and tell <https://github.com/ThreeMammals/Ocelot/discussions/categories/show-and-tell>`_\" discussion in the repository.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The \":ref:`Single Authentication Scheme <authentication-scheme>`\" feature has been an Ocelot artifact for ages. Use the ``AuthenticationProviderKeys`` property instead of ``AuthenticationProviderKey`` one. We support this ``[Obsolete]`` property for backward compatibility and migration reasons. In future releases, the property may be removed as a breaking change.\r\n.. [#f2] The \":ref:`Multiple Authentication Schemes <authentication-multiple>`\" feature was requested in issues `740`_, `1580`_ and delivered as a part of `23.0`_ release.\r\n.. [#f3] The global \":ref:`Configuration and AllowAnonymous <authentication-configuration>`\" feature for static routes was requested in issues `842`_ and `1414`_, implemented in pull request `2114`_, and officially released in version `24.1`_.\r\n.. [#f4] The \":ref:`Global Configuration <authentication-global-configuration>`\" feature for dynamic routes was requested in issues `585`_ and `2316`_, implemented in pull request `2336`_, and released in version `24.1`_.\r\n.. [#f5] The \":ref:`authentication-allowed-scopes`\" feature fully supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the ``scope`` claim in the ``ScopesAuthorizer`` service, which is part of the :ref:`authorization-middleware`.\r\n  Refer to section `4.2. \"scope\" (Scopes) Claim`_.\r\n  This enhancement was requested in bug `913`_, fixed in pull request `1478`_, and the patch was rolled out as part of the `24.1`_ release.\r\n\r\n.. _RFC 8693: https://datatracker.ietf.org/doc/html/rfc8693\r\n.. _4.2. \"scope\" (Scopes) Claim: https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim\r\n\r\n.. _446: https://github.com/ThreeMammals/Ocelot/issues/446\r\n.. _585: https://github.com/ThreeMammals/Ocelot/issues/585\r\n.. _740: https://github.com/ThreeMammals/Ocelot/issues/740\r\n.. _842: https://github.com/ThreeMammals/Ocelot/issues/842\r\n.. _913: https://github.com/ThreeMammals/Ocelot/issues/913\r\n.. _1414: https://github.com/ThreeMammals/Ocelot/issues/1414\r\n.. _1478: https://github.com/ThreeMammals/Ocelot/pull/1478\r\n.. _1580: https://github.com/ThreeMammals/Ocelot/issues/1580\r\n.. _2114: https://github.com/ThreeMammals/Ocelot/pull/2114\r\n.. _2316: https://github.com/ThreeMammals/Ocelot/issues/2316\r\n.. _2336: https://github.com/ThreeMammals/Ocelot/pull/2336\r\n.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13\r\n"
  },
  {
    "path": "docs/features/authorization.rst",
    "content": "Authorization\r\n=============\r\n\r\nOcelot supports claims based authorization which is run post authentication.\r\nThis means if you have a route you want to authorize, you can add the following to your route configuration:\r\n\r\n.. code-block:: json\r\n\r\n  \"RouteClaimsRequirement\": {\r\n    \"UserType\": \"registered\"\r\n  }\r\n\r\nIn this example, when the :ref:`authorization-middleware` is called, Ocelot will check to see if the user has the claim type ``UserType`` and if the value of that claim is ``\"registered\"``.\r\nIf it isn't then the user will not be authorized and the response will be `403 Forbidden <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403>`_.\r\n\r\n.. _authorization-middleware:\r\n\r\nAuthorization Middleware\r\n------------------------\r\n\r\nThe `AuthorizationMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+AuthorizationMiddleware+language%3AC%23&type=code&l=C%23>`_ is built-in into Ocelot pipeline.\r\n\r\n  | Previous private: ``ClaimsToClaimsMiddleware``\r\n  | Previous public: ``PreAuthorizationMiddleware``\r\n  | **This**: ``AuthorizationMiddleware``\r\n  | Next private: ``ClaimsToHeadersMiddleware``\r\n  | Next public: ``PreQueryStringBuilderMiddleware``\r\n\r\n.. role::  htm(raw)\r\n    :format: html\r\n\r\nSo, the closest middlewares are in order of calling:\r\n\r\n``ClaimsToClaimsMiddleware`` :htm:`&rarr;` ``PreAuthorizationMiddleware`` :htm:`&rarr;` **AuthorizationMiddleware** :htm:`&rarr;` ``ClaimsToHeadersMiddleware`` :htm:`&rarr;` ``PreQueryStringBuilderMiddleware``\r\n\r\nAs you may know from the :doc:`../features/middlewareinjection` chapter, the Authorization middleware can be overridden like this:\r\n\r\n.. code-block:: csharp\r\n\r\n    var app = builder.Build();\r\n    await app.UseOcelot(new OcelotPipelineConfiguration\r\n    {\r\n        AuthorizationMiddleware = async (context, next) =>\r\n        {\r\n            await next.Invoke();\r\n        }\r\n    });\r\n    await app.RunAsync();\r\n\r\n**Note!** Do this in very rare cases, because overriding the Authorization middleware means you will lose claims and scopes authorizer through the ``RouteClaimsRequirement`` property of the route.\r\nAnother option is preparing before the actual authorization in ``PreAuthorizationMiddleware``, which is public and open to overriding.\r\n\r\n.. code-block:: csharp\r\n\r\n    await app.UseOcelot(new OcelotPipelineConfiguration\r\n    {\r\n        PreAuthorizationMiddleware = async (context, next) =>\r\n        {\r\n            // Do whatever you want here\r\n            await next.Invoke(); // next is AuthorizationMiddleware\r\n        }\r\n    });\r\n"
  },
  {
    "path": "docs/features/caching.rst",
    "content": ".. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n\r\nCaching\r\n=======\r\n\r\n[#f1]_ Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache.\r\nUsers can also clear the cache for a specific region by using Ocelot's :ref:`administration-api`.\r\n\r\nOcelot utilizes some very rudimentary caching at the moment provider by the `CacheManager <https://github.com/MichaCo/CacheManager>`_ project.\r\nThis is an amazing project that is solving a lot of caching problems. We would recommend using this package to cache with Ocelot. \r\n\r\nThe following example shows how to add *CacheManager* to Ocelot so that you can do output caching. \r\n\r\nInstall\r\n-------\r\n\r\nFirst of all, add the following `Ocelot.Cache.CacheManager <https://www.nuget.org/packages/Ocelot.Cache.CacheManager>`_ package:\r\n\r\n.. code-block:: powershell\r\n\r\n    Install-Package Ocelot.Cache.CacheManager\r\n\r\nThis will give you access to the Ocelot cache manager extension methods.\r\nThe second step is to add the following to your `Program`_:\r\n\r\n.. code-block:: csharp\r\n\r\n    using Ocelot.Cache.CacheManager;\r\n\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddCacheManager(x => x.WithDictionaryHandle());\r\n\r\n``CacheOptions`` Schema\r\n-----------------------\r\n\r\n.. _FileCacheOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileCacheOptions.cs\r\n\r\n  Class: `FileCacheOptions`_\r\n\r\nThe following is the full *caching* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`.\r\nNot all of these options need to be configured; however, the ``TtlSeconds`` option is mandatory.\r\n\r\n.. code-block:: json\r\n\r\n  \"CacheOptions\": {\r\n    \"TtlSeconds\": 1, // nullable integer\r\n    \"Region\": \"\", // string\r\n    \"Header\": \"\", // string\r\n    \"EnableContentHashing\": false // nullable boolean\r\n  }\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - *Option*\r\n    - *Description*\r\n  * - ``TtlSeconds``\r\n    - Time-To-Live (TTL) in seconds for the cached downstream response, i.e., the absolute expiration timeout starting from when the item is added to the cache.\r\n      This option is required. If undefined, it defaults to 0 (zero), which disables caching.\r\n  * - ``Region``\r\n    - Specifies the cache region to be cleared via Ocelot's :ref:`administration-api`.\r\n      See: ``DELETE {adminPath}/outputcache/{region}``\r\n  * - ``Header``\r\n    - Specifies the header name used for native Ocelot caching control, defaulting to the special ``OC-Cache-Control`` header.\r\n      If the header is present, its value is included in the cache key constructed by the ``ICacheKeyGenerator`` service.\r\n      Varying header values result in different cache keys, effectively invalidating the cache.\r\n\r\n  * - ``EnableContentHashing``\r\n    - Toggles inclusion of request body hashing in the cache key.\r\n      Disabled by default (``false``) due to potential performance impact.\r\n      Recommended for POST/PUT routes where request body affects response.\r\n      Refer to the :ref:`EnableContentHashing option <caching-enablecontenthashing-option>` section.\r\n\r\nThe actual ``CacheOptions`` schema with all the properties can be found in the C# `FileCacheOptions`_ class.\r\n\r\n.. _caching-configuration:\r\n\r\nConfiguration\r\n-------------\r\n\r\nFinally, in order to use caching on a route in your route configuration add these sections:\r\n\r\n.. code-block:: json\r\n\r\n    \"CacheOptions\": {\r\n      \"TtlSeconds\": 15,\r\n      \"Region\": \"europe-central\",\r\n      \"Header\": \"OC-Cache-Control\",\r\n      \"EnableContentHashing\": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc.\r\n    },\r\n    // Warning! FileCacheOptions section is deprecated! -> use CacheOptions\r\n    \"FileCacheOptions\": {\r\n      \"TtlSeconds\": 15,\r\n      \"Region\": \"europe-central\",\r\n      \"Header\": \"OC-Cache-Control\",\r\n      \"EnableContentHashing\": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc.\r\n    }\r\n\r\n* In this example, ``TtlSeconds`` is set to 15, which means the cache will expire 15 seconds after the response is stored.\r\n* The ``Region`` property specifies a cache region. Cache entries within a region can be cleared by calling Ocelot's :ref:`administration-api`.\r\n* If a header name is defined in the ``Header`` property, its value is retrieved from the ``HttpRequest`` headers.\r\n  If the header is present, its value is included in the cache key constructed by the ``ICacheKeyGenerator`` service.\r\n  Varying header values result in different cache keys, effectively invalidating the cache.\r\n* Finally, ``EnableContentHashing`` is disabled due to the current route using the ``GET`` verb, which does not include a request body.\r\n\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13\r\n.. warning::\r\n  According to the static :ref:`config-route-schema`, the ``FileCacheOptions`` section has been deprecated!\r\n\r\n  The `old schema <https://github.com/ThreeMammals/Ocelot/blob/24.1.0/src/Ocelot/Configuration/File/FileRoute.cs#L86-L88>`_ ``FileCacheOptions`` section is deprecated in version `24.1`_!\r\n  Use ``CacheOptions`` instead of ``FileCacheOptions``! Note that ``FileCacheOptions`` will be removed in version `25.0`_!\r\n  For backward compatibility in version `24.1`_, the ``FileCacheOptions`` section takes precedence over the ``CacheOptions`` section.\r\n\r\n.. _caching-enablecontenthashing-option:\r\n\r\n``EnableContentHashing`` option [#f2]_\r\n--------------------------------------\r\n\r\nPreviously, in versions prior to `23.0`_, the request body was used to compute the cache key.\r\nHowever, due to potential performance issues arising from request body hashing, it has been disabled by default.\r\nClearly, this constitutes a breaking change and presents challenges for users who require cache key calculations that consider the request body (e.g., for the POST method).\r\nTo address this issue, it is recommended to enable the option either at the route level or globally in the \":ref:`Global Configuration <caching-global-configuration>`\" section:\r\n\r\n.. code-block:: json\r\n\r\n    \"CacheOptions\": {\r\n      // ...\r\n      \"EnableContentHashing\": true\r\n    }\r\n\r\n.. rubric:: Ocelot Team Recommendation\r\n\r\nAlthough the community raised concerns about backward compatibility in issue `2234`_, Ocelot team maintains that *caching* performance takes precedence over backward compatibility when migrating from versions prior to `23.0`_.\r\nThe proposed option clarifies that ``POST`` requests should **not** be cached; only ``GET`` requests are eligible for caching.\r\nTherefore, ``POST`` and ``GET`` verbs must be separated into distinct routes:\r\n\r\n* POST routes with *caching* disabled\r\n* GET routes with *caching* enabled\r\n\r\n.. _caching-global-configuration:\r\n\r\nGlobal Configuration [#f3]_\r\n---------------------------\r\n\r\nCopying route-level properties for each static route is no longer necessary, as version `23.3`_ allows these values to be set in the ``GlobalConfiguration`` section.\r\nThis convenience applies to ``Header`` and ``Region`` as well.\r\nHowever, if no global ``TtlSeconds`` value is defined, this option must still be explicitly set per route to enable caching.\r\nAs a result, the final configuration for static routes might look like:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 5-7, 12, 18-21\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        \"Key\": \"R0\", // optional\r\n        \"CacheOptions\": {\r\n          \"TtlSeconds\": 60 // 1-minute short-term caching\r\n        },\r\n        // ...\r\n      },\r\n      {\r\n        \"Key\": \"R1\", // this route is part of a group\r\n        \"CacheOptions\": {}, // optional due to grouping\r\n        // ...\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://ocelot.net\",\r\n      \"CacheOptions\": {\r\n        \"RouteKeys\": [\"R1\",], // if undefined or empty array, opts will apply to all routes\r\n        \"TtlSeconds\": 300 // enable global caching for a duration of 5 minutes\r\n      },\r\n      // ...\r\n    }\r\n  }\r\n\r\nDynamic routes were not supported in versions prior to `24.1`_.\r\nStarting with version `24.1`_, global *cache options* for :ref:`Dynamic Routing <routing-dynamic>` were introduced.\r\nThese global options may also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 6-8, 17-20\r\n\r\n  {\r\n    \"DynamicRoutes\": [\r\n      {\r\n        \"Key\": \"\", // optional\r\n        \"ServiceName\": \"my-service\",\r\n        \"CacheOptions\": {\r\n          \"TtlSeconds\": 60 // 1-minute short-term caching\r\n        }\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://ocelot.net\",\r\n      \"DownstreamScheme\": \"http\",\r\n      \"ServiceDiscoveryProvider\": {\r\n        // required section for dynamic routing\r\n      },\r\n      \"CacheOptions\": {\r\n        \"RouteKeys\": [], // or null, no grouping, thus opts apply to all dynamic routes\r\n        \"TtlSeconds\": 300 // enable global caching for a duration of 5 minutes\r\n      }\r\n    }\r\n  }\r\n\r\nIn this configuration, a 5-minute *caching* duration is applied to all implicit dynamic routes.\r\nHowever, for the \"my-service\" service, the *caching* TTL has been explicitly reduced from 5 minutes to 1 minute.\r\n\r\n.. note::\r\n\r\n  1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``CacheOptions``, the global options will apply to all routes.\r\n  If the array contains route keys, it defines a single group of routes to which the global options apply.\r\n  Routes excluded from this group must specify their own route-level ``CacheOptions``.\r\n\r\n  2. Prior to version `23.3`_, global ``CacheOptions`` were not available.\r\n  Starting with version `24.1`_, global configuration is supported for both static and dynamic routes.\r\n\r\n.. Sample\r\n.. -----\r\n\r\n.. If you look at the example `here <https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/Program.cs>`_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method.\r\n.. You can use any settings supported by the **CacheManager** package and just pass them in.\r\n\r\nCustom Caching\r\n--------------\r\n\r\nIf you want to add your own caching method, implement the following interfaces and register them in DI e.g.\r\n\r\n.. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddSingleton<IOcelotCache<CachedResponse>, MyCache>();\r\n\r\n* ``IOcelotCache<CachedResponse>`` this is for output caching.\r\n* ``IOcelotCache<FileConfiguration>`` this is for caching the file configuration if you are calling something remote to get your config such as Consul.\r\n\r\nRoadmap\r\n-------\r\n\r\nPlease dig into the Ocelot source code to find more.\r\nWe would really appreciate it if anyone wants to implement `Redis <https://redis.io/>`_, `Memcached <http://www.memcached.org/>`_ etc.\r\nPlease, open a new `Show and tell <https://github.com/ThreeMammals/Ocelot/discussions/categories/show-and-tell>`_ thread in `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ space of the repository.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] Historically, *Caching* is one of Ocelot's earliest features, first introduced in version `1.1`_ on February 2, 2017, the initial release of Ocelot.\r\n  The \"Clear cache region via :ref:`administration-api`\" feature was first delivered in pull request `109`_ and released in version `1.4.8`_.\r\n.. [#f2] The \":ref:`EnableContentHashing option <caching-enablecontenthashing-option>`\" feature was requested in issue `2059`_ and released in version `23.3`_.\r\n.. [#f3] :ref:`Global Configuration <caching-global-configuration>` for static routes was first introduced in pull request `2058`_ and released in version `23.3`_.\r\n  Support for dynamic routes was added in pull request `2331`_ and delivered in version `24.1`_.\r\n\r\n.. _109: https://github.com/ThreeMammals/Ocelot/pull/109\r\n.. _2058: https://github.com/ThreeMammals/Ocelot/pull/2058\r\n.. _2059: https://github.com/ThreeMammals/Ocelot/issues/2059\r\n.. _2234: https://github.com/ThreeMammals/Ocelot/issues/2234\r\n.. _2331: https://github.com/ThreeMammals/Ocelot/pull/2331\r\n\r\n.. _1.1: https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\r\n.. _1.4.8: https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.8\r\n.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n"
  },
  {
    "path": "docs/features/claimstransformation.rst",
    "content": "Claims Transformation\r\n=====================\r\n\r\nOcelot allows the user to access claims and transform them into headers, query string parameters, other claims and change downstream paths. This is only available once a user has been authenticated.\r\n\r\nAfter the user is authenticated, we run the claims to claims transformation middleware (see the `ClaimsToClaimsMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20ClaimsToClaimsMiddleware&type=code>`_ class).\r\nThis allows the user to transform claims before the authorization middleware is called.\r\nAfter the user is authorized, we call the claims to headers middleware (see the `ClaimsToHeadersMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+ClaimsToHeadersMiddleware&type=code>`_ class),\r\nthen the claims to query string parameters middleware (see the `ClaimsToQueryStringMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+ClaimsToQueryStringMiddleware&type=code>`_ class),\r\nand finally the claims to downstream path middleware (see the `ClaimsToDownstreamPathMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+ClaimsToDownstreamPathMiddleware&type=code>`_ class).\r\n\r\nThe syntax for performing the transforms is the same for each process.\r\nIn the route configuration, a JSON dictionary is added with a specific name either ``AddClaimsToRequest``, ``AddHeadersToRequest``, ``AddQueriesToRequest``, or ``ChangeDownstreamPathTemplate``.\r\n\r\n    **Note**: This syntax is not ideal. So any suggestions are welcome...\r\n\r\nWithin this dictionary the entries specify how Ocelot should transform things!\r\nThe key to the dictionary is going to become the key of either a claim, header or query parameter.\r\nIn the case of ``ChangeDownstreamPathTemplate``, the key must be also specified in the ``DownstreamPathTemplate``, in order to do the transformation.\r\n\r\nThe value of the entry is parsed to logic that will perform the transform.\r\nFirst of all, a dictionary accessor is specified e.g. ``Claims[CustomerId]``. This means we want to access the claims and get the ``CustomerId`` claim type.\r\nNext is a \"greater than\" ``>`` symbol which is just used to split the string. The next entry is either value or value with an indexer.\r\nIf value is specified, Ocelot will just take the value and add it to the transform.\r\nIf the value has an indexer, Ocelot will look for a delimiter which is provided after another \"greater than\" ``>`` symbol.\r\nOcelot will then split the value on the delimiter and add whatever was at the index requested to the transform.\r\n\r\nClaims to Claims\r\n----------------\r\n\r\nBelow is an example configuration that will transform claims to claims\r\n\r\n.. code-block:: json\r\n\r\n  \"AddClaimsToRequest\": {\r\n    \"UserType\": \"Claims[sub] > value[0] > |\",\r\n    \"UserId\": \"Claims[sub] > value[1] > |\"\r\n  }\r\n\r\nThis shows a transforms where Ocelot looks at the users ``sub`` claim and transforms it into ``UserType`` and ``UserId`` claims.\r\nAssuming the ``sub`` looks like this ``usertypevalue|useridvalue``.\r\n\r\nClaims to Headers\r\n-----------------\r\n\r\nBelow is an example configuration that will transform claims to headers\r\n\r\n.. code-block:: json\r\n\r\n  \"AddHeadersToRequest\": {\r\n    \"CustomerId\": \"Claims[sub] > value[1] > |\"\r\n  }\r\n\r\nThis shows a transform where Ocelot looks at the users ``sub`` claim and transforms it into a ``CustomerId`` header.\r\nAssuming the ``sub`` looks like this ``usertypevalue|useridvalue``.\r\n\r\nClaims to Query String Parameters\r\n---------------------------------\r\n\r\nBelow is an example configuration that will transform claims to query string parameters\r\n\r\n.. code-block:: json\r\n\r\n  \"AddQueriesToRequest\": {\r\n    \"LocationId\": \"Claims[LocationId] > value\",\r\n  }\r\n\r\nThis shows a transform where Ocelot looks at the users ``LocationId`` claim and add it as a query string parameter to be forwarded onto the downstream service.\r\n\r\nClaims to Downstream Path\r\n-------------------------\r\n\r\nBelow is an example configuration that will transform claims to downstream path custom placeholders:\r\n\r\n.. code-block:: json\r\n\r\n  \"UpstreamPathTemplate\": \"/api/users/me/{everything}\",\r\n  \"DownstreamPathTemplate\": \"/api/users/{userId}/{everything}\",\r\n  \"ChangeDownstreamPathTemplate\": {\r\n    \"userId\": \"Claims[sub] > value[1] > |\",\r\n  }\r\n\r\nThis shows a transform where Ocelot looks at the users ``userId`` claim and substitutes the value to the ``{userId}`` placeholder specified in the ``DownstreamPathTemplate``.\r\nTake into account that the key specified in the ``ChangeDownstreamPathTemplate`` must be the same than the placeholder specified in the ``DownstreamPathTemplate``.\r\n\r\n    **Note**: If a key specified in the ``ChangeDownstreamPathTemplate`` does not exist as a placeholder in ``DownstreamPathTemplate``, it will fail at runtime returning an error in the response.\r\n"
  },
  {
    "path": "docs/features/configuration.rst",
    "content": ".. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Configuration/Program.cs\r\n.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs\r\n.. _Consul: https://www.consul.io/\r\n.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv\r\n\r\nConfiguration\r\n=============\r\n\r\nAn example configuration can be found here in `ocelot.json`_.\r\nThere are two major sections to the configuration: an array of ``Routes`` and a ``GlobalConfiguration`` sections:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Routes\": [],\r\n    \"GlobalConfiguration\": {}\r\n  }\r\n\r\nFrom the :doc:`../introduction/gettingstarted` chapter and its :ref:`getstarted-configuration` section, you may already know that there are four total configuration sections:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Routes\": [], // static routes\r\n    \"DynamicRoutes\": [],\r\n    \"Aggregates\": [], // BFF\r\n    \"GlobalConfiguration\": {}\r\n  }\r\n\r\n.. list-table::\r\n    :widths: 25 75\r\n    :header-rows: 1\r\n\r\n    * - *Section*\r\n      - *Description*\r\n    * - ``Routes`` with :ref:`config-route-schema`\r\n      - The static objects that tell Ocelot how to treat an upstream request.\r\n        Once static routes have been loaded during gateway startup, in general, they cannot be changed during the lifetime of the app instance, with a few exceptional use cases.\r\n    * - ``DynamicRoutes`` with :ref:`config-dynamic-route-schema`\r\n      - This section enables dynamic routing when using a :doc:`../features/servicediscovery` provider.\r\n        Please refer to the :ref:`routing-dynamic` docs for more details.\r\n    * - ``Aggregates`` with :ref:`config-aggregate-route-schema`\r\n      - This section allows specifying aggregated routes that compose multiple normal routes and map their responses into one JSON object.\r\n        It allows you to start implementing a *Back-end For a Front-end* (BFF) type architecture with Ocelot.\r\n        Please refer to the :doc:`../features/aggregation` chapter for more details.\r\n    * - ``GlobalConfiguration`` with :ref:`config-global-configuration-schema`\r\n      - This section is a bit hacky and allows overrides of static route-specific settings.\r\n        It is useful if you do not want to manage lots of route-specific settings.\r\n\r\nTo fully understand all configuration capabilities, we recommend reading all sections below.\r\n\r\n.. _config-route-schema:\r\n\r\nRoute Schema\r\n------------\r\n\r\n.. _FileRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRoute.cs\r\n\r\n    Class: `FileRoute`_\r\n\r\nHere is the complete route configuration, also known as the *\"route schema,\"* of top-level properties.\r\nYou do not need to set all of these things, but this is everything that is available at the moment.\r\n\r\n.. code-block:: json\r\n\r\n    {\r\n      \"AddClaimsToRequest\": {}, // dictionary\r\n      \"AddHeadersToRequest\": {}, // dictionary\r\n      \"AddQueriesToRequest\": {}, // dictionary\r\n      \"AuthenticationOptions\": {}, // object\r\n      \"ChangeDownstreamPathTemplate\": {}, // dictionary\r\n      \"DangerousAcceptAnyServerCertificateValidator\": false,\r\n      \"DelegatingHandlers\": [], // array of strings\r\n      \"DownstreamHeaderTransform\": {}, // dictionary\r\n      \"DownstreamHostAndPorts\": [], // array of FileHostAndPort\r\n      \"DownstreamHttpMethod\": \"\",\r\n      \"DownstreamHttpVersion\": \"\",\r\n      \"DownstreamHttpVersionPolicy\": \"\",\r\n      \"DownstreamPathTemplate\": \"\",\r\n      \"DownstreamScheme\": \"\",\r\n      \"CacheOptions\": {}, // object\r\n      \"FileCacheOptions\": {}, // deprecated! -> use CacheOptions\r\n      \"HttpHandlerOptions\": {}, // object\r\n      \"Key\": \"\",\r\n      \"LoadBalancerOptions\": {}, // object\r\n      \"Metadata\": {}, // dictionary\r\n      \"Priority\": 1, // integer\r\n      \"QoSOptions\": {}, // object\r\n      \"RateLimitOptions\": {}, // object\r\n      \"RequestIdKey\": \"\",\r\n      \"RouteClaimsRequirement\": {}, // dictionary\r\n      \"RouteIsCaseSensitive\": false,\r\n      \"SecurityOptions\": {}, // object\r\n      \"ServiceName\": \"\",\r\n      \"ServiceNamespace\": \"\",\r\n      \"Timeout\": 0, // nullable integer\r\n      \"UpstreamHeaderTemplates\": {}, // dictionary\r\n      \"UpstreamHeaderTransform\": {}, // dictionary\r\n      \"UpstreamHost\": \"\",\r\n      \"UpstreamHttpMethod\": [], // array of strings\r\n      \"UpstreamPathTemplate\": \"\"\r\n    },\r\n\r\nThe actual route schema with all the properties can be found in the C# `FileRoute`_ class.\r\n\r\n  **Note**: The `old schema <https://github.com/ThreeMammals/Ocelot/blob/24.1.0/src/Ocelot/Configuration/File/FileRoute.cs#L86-L88>`__ ``FileCacheOptions`` section is deprecated in version `24.1`_!\r\n  Use ``CacheOptions`` instead of ``FileCacheOptions``! Note that ``FileCacheOptions`` will be removed in version `25.0`_!\r\n  For backward compatibility in version `24.1`_, the ``FileCacheOptions`` section takes precedence over the ``CacheOptions`` section.\r\n\r\n.. _config-dynamic-route-schema:\r\n\r\nDynamic Route Schema\r\n--------------------\r\n\r\n.. _FileDynamicRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileDynamicRoute.cs\r\n\r\n    Class: `FileDynamicRoute`_\r\n\r\nHere is the complete dynamic route configuration, also known as the *\"dynamic route schema,\"* of top-level properties.\r\n\r\n.. code-block:: json\r\n\r\n    {\r\n      \"AuthenticationOptions\": {},\r\n      \"CacheOptions\": {},\r\n      \"DownstreamHttpVersion\": \"\",\r\n      \"DownstreamHttpVersionPolicy\": \"\",\r\n      \"HttpHandlerOptions\": {},\r\n      \"LoadBalancerOptions\": {},\r\n      \"Metadata\": {}, // dictionary\r\n      \"QoSOptions\": {},\r\n      \"RateLimitRule\": {}, // deprecated! -> use RateLimitOptions\r\n      \"RateLimitOptions\": {},\r\n      \"ServiceName\": \"\",\r\n      \"ServiceNamespace\": \"\",\r\n      \"Timeout\": 0 // nullable integer\r\n    }\r\n\r\nThe actual dynamic route schema with all the properties can be found in the C# `FileDynamicRoute`_ class.\r\n\r\n  **Note 1**: The `old schema <https://github.com/ThreeMammals/Ocelot/blob/24.1.0/src/Ocelot/Configuration/File/FileDynamicRoute.cs#L10-L11>`_ ``RateLimitRule`` section is deprecated in version `24.1`_!\r\n  Use ``RateLimitOptions`` instead of ``RateLimitRule``! Note that ``RateLimitRule`` will be removed in version `25.0`_!\r\n  For backward compatibility in version `24.1`_, the ``RateLimitRule`` section takes precedence over the ``RateLimitOptions`` section.\r\n\r\n  **Note 2**: The following options were not supported in versions prior to `24.1`_ for overriding globally configured options: ``AuthenticationOptions``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, ``ServiceNamespace``, and ``Timeout``.\r\n  Starting with version `24.1`_, both global and route-level options for :ref:`Dynamic Routing <routing-dynamic>` were introduced.\r\n  For a clearer understanding of the changes, refer to the `previous schema (version 24.0) <https://ocelot.readthedocs.io/en/24.0/features/configuration.html#dynamic-route-schema>`_.\r\n\r\n.. _config-aggregate-route-schema:\r\n\r\nAggregate Route Schema\r\n----------------------\r\n\r\n.. _FileAggregateRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileAggregateRoute.cs\r\n\r\n    Class: `FileAggregateRoute`_\r\n\r\nHere is the complete aggregated route configuration, also known as the *\"aggregate route schema,\"* of top-level properties.\r\n\r\n.. code-block:: json\r\n\r\n    {\r\n      \"Aggregator\": \"\",\r\n      \"Priority\": 1, // integer\r\n      \"RouteIsCaseSensitive\": false,\r\n      \"RouteKeys\": [], // array of strings\r\n      \"RouteKeysConfig\": [], // array of AggregateRouteConfig\r\n      \"UpstreamHeaderTemplates\": {}, // dictionary\r\n      \"UpstreamHost\": \"\",\r\n      \"UpstreamHttpMethod\": [], // array of strings\r\n      \"UpstreamPathTemplate\": \"\"\r\n    }\r\n\r\nThe actual aggregated route schema with all the properties can be found in the C# `FileAggregateRoute`_ class.\r\n\r\n.. _config-global-configuration-schema:\r\n\r\nGlobal Configuration Schema\r\n---------------------------\r\n\r\n.. _FileGlobalConfiguration: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs\r\n\r\n    Class: `FileGlobalConfiguration`_\r\n\r\nHere is the complete global configuration, also known as the *\"global configuration schema,\"* of top-level properties.\r\n\r\n.. code-block:: json\r\n\r\n    {\r\n      \"AuthenticationOptions\": {},\r\n      \"BaseUrl\": \"\",\r\n      \"CacheOptions\": {},\r\n      \"DownstreamHeaderTransform\": {}, // dictionary\r\n      \"DownstreamHttpVersion\": \"\",\r\n      \"DownstreamHttpVersionPolicy\": \"\",\r\n      \"DownstreamScheme\": \"\",\r\n      \"HttpHandlerOptions\": {},\r\n      \"LoadBalancerOptions\": {},\r\n      \"Metadata\": {}, // dictionary\r\n      \"MetadataOptions\": {},\r\n      \"QoSOptions\": {},\r\n      \"RateLimitOptions\": {},\r\n      \"RequestIdKey\": \"\",\r\n      \"SecurityOptions\": {},\r\n      \"ServiceDiscoveryProvider\": {},\r\n      \"Timeout\": 0, // nullable integer\r\n      \"UpstreamHeaderTransform\": {} // dictionary\r\n    }\r\n\r\nThe actual global configuration schema with all the properties can be found in the C# `FileGlobalConfiguration`_ class.\r\n\r\n  **Note 1**: The following global options were not supported in versions prior to `24.1`_ for overriding in the :ref:`config-dynamic-route-schema`: ``AuthenticationOptions``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, and ``Timeout``.\r\n  Moreover, these global options were not available in versions prior to `24.1`_ for static routes, as stated in issue `585`_.\r\n  Starting with version `24.1`_, both static and dynamic route *global* options are fully supported.\r\n  For a clearer understanding of the changes, refer to the :ref:`config-dynamic-route-schema` and related notes.\r\n\r\n  **Note 2**: The ``DownstreamHeaderTransform`` and ``UpstreamHeaderTransform`` global options were introduced in version `24.1`_, but they are available only for static routes.\r\n\r\n.. _config-overview:\r\n\r\nConfiguration Overview\r\n----------------------\r\n\r\n:doc:`../features/dependencyinjection` of the *Configuration* feature in Ocelot allows you to extend, manage, and build Ocelot Core *configuration* **before** the stage of building ASP.NET Core services.\r\n\r\nTo configure the Ocelot Core and services, use the following abstract program-structure, which must be presented in your `Program`_:\r\n\r\n1. **Create application builder**: The ``Microsoft.AspNetCore.Builder.WebApplication`` has three overloaded versions of the ``CreateBuilder()`` methods.\r\n   Our recommendation is to utilize arguments possibly coming from terminal sessions into an app host; thus, use the ``CreateBuilder(args)`` method.\r\n\r\n  .. code-block:: csharp\r\n\r\n      var builder = WebApplication.CreateBuilder(args);\r\n\r\n2. **Set up the configuration builder**: Utilize the ``WebApplicationBuilder.Configuration`` property, which returns a ``ConfigurationManager`` object implementing the target ``IConfigurationBuilder`` interface.\r\n\r\n  .. code-block:: csharp\r\n\r\n      builder.Configuration.AddOcelot(...);\r\n\r\n3. **Forward configuration to the Ocelot builder**: The ``Ocelot.DependencyInjection.ServiceCollectionExtensions`` class has three overloaded versions of the ``AddOcelot(IServiceCollection)`` methods, which return an ``IOcelotBuilder`` object.\r\n\r\n  .. code-block:: csharp\r\n\r\n      builder.Services.AddOcelot(builder.Configuration);\r\n\r\n4. **Finish the app setup**, add middlewares, and finally run the application: Let's write the final algorithm.\r\n\r\n  .. code-block:: csharp\r\n\r\n      var builder = WebApplication.CreateBuilder(args); // step 1\r\n      builder.Configuration.AddOcelot(...); // step 2\r\n      builder.Services.AddOcelot(builder.Configuration); // step 3\r\n\r\n      // Step 4\r\n      var app = builder.Build();\r\n      await app.UseOcelot();\r\n      await app.RunAsync();\r\n\r\nFor comprehensive documentation of configuration DI-extensions, please refer to the :ref:`di-configuration-overview` section in the :doc:`../features/dependencyinjection` chapter.\r\n\r\nMultiple Environments\r\n---------------------\r\n\r\nLike any other ASP.NET Core project Ocelot supports configuration file names such as ``appsettings.dev.json``, ``appsettings.test.json`` etc.\r\nIn order to implement this add the following to you:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 4,5,7\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddJsonFile(\"ocelot.json\") // primary config file\r\n        .AddJsonFile($\"ocelot.{builder.Environment.EnvironmentName}.json\");\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\nOcelot will now use the environment specific configuration and fall back to `ocelot.json`_ if there isn't one.\r\nAnother version of the configuration above, which is based on configuration providers, is the following:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 4,6,7,9\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddOcelot() // single ocelot.json file without environment one\r\n        // or\r\n        .AddOcelot(builder.Environment)\r\n        .AddJsonFile($\"ocelot.{builder.Environment.EnvironmentName}.json\");\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\nYou also need to set the corresponding ``ASPNETCORE_ENVIRONMENT`` variable.\r\n\r\n    **Note 1**: More info on configuration can be found in the ASP.NET Core documentation:\r\n\r\n    * `Use multiple environments in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments>`_\r\n    * `Configuration in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/>`_\r\n\r\n    **Note 2**: Calling the following configuration methods is rudimentary in ASP.NET Core because of internal encapsulation in the default builder, aka ``CreateBuilder(args)`` method.\r\n\r\n    .. code-block:: csharp\r\n      :emphasize-lines: 3,4,5\r\n\r\n        var builder = WebApplication.CreateBuilder(args);\r\n        builder.Configuration\r\n            .AddJsonFile(\"appsettings.json\", true, true) // not required\r\n            .AddJsonFile($\"appsettings.{builder.Environment.EnvironmentName}.json\", true, true) // not required\r\n            .AddEnvironmentVariables() // not required\r\n            // ...\r\n\r\n    This is explained in the `Default application configuration sources <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-9.0#default-application-configuration-sources>`_ docs; thus, remove these optional methods.\r\n\r\n.. _config-merging-files:\r\n\r\nMerging Files [#f1]_\r\n--------------------\r\n\r\n  **Sample**: `Ocelot.Samples.Configuration <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Configuration/>`_\r\n\r\nThis feature allows users to have multiple configuration files to make managing large configurations easier.\r\n\r\nRather than directly adding the configuration e.g., using ``AddJsonFile(\"ocelot.json\")``, you can achieve the same result by invoking ``AddOcelot()`` as shown below:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddOcelot(builder.Environment); // will skip environment file\r\n\r\nIn this scenario, Ocelot will look for any files that match the pattern ``^ocelot\\.(.*?)\\.json$`` as the regular expression and then merge these together.\r\nThe environment file will be skipped aka ``ocelot.{builder.Environment.EnvironmentName}.json``.\r\nIf you want to set the ``GlobalConfiguration`` property, you must have a file called ``ocelot.global.json``.\r\n\r\nThe way Ocelot merges the files is basically load them, loop over them, skip environment file, add any ``Routes``, add any ``AggregateRoutes`` and if the file is called ``ocelot.global.json`` add the ``GlobalConfiguration`` aswell as any ``Routes`` or ``AggregateRoutes``.\r\nOcelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running.\r\n\r\n  **Note 1**: Currently, validation occurs only during the final merging of configurations in Ocelot.\r\n  It's essential to be aware of this when troubleshooting issues.\r\n  We recommend thoroughly inspecting the contents of the ``ocelot.json`` file if you encounter any problems.\r\n\r\n  **Note 2**: The Merging feature is operational only during the application's startup.\r\n  Consequently, the merged configuration in ``ocelot.json`` remains static post-merging and startup.\r\n  Once the Ocelot application has started, you cannot call the ``AddOcelot`` method, nor can you employ the merging feature within ``AddOcelot``.\r\n  If you still require on-the-fly updating of the primary configuration file, ``ocelot.json``, please refer to the :ref:`config-react-to-changes` section.\r\n  Additionally, note that merging partial configuration files (such as ``ocelot.*.json``) on the fly using :doc:`../features/administration` API is not currently implemented.\r\n\r\n  **Note 3**: An alternative to static merged configurations could be the construction of the ``FileConfiguration`` object before passing it as an argument to the :ref:`di-configuration-addocelot-methods` method.\r\n  Refer to the :ref:`config-build-from-scratch` subsection for details.\r\n\r\nKeep files in a folder\r\n^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nYou can also give Ocelot a specific path to look in for the configuration files as shown below:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddOcelot(\"/my/folder\", builder.Environment); // happy path\r\n\r\nOcelot needs the ``builder.Environment`` so it knows to exclude any environment-specific files from the merging algorithm, such as ``ocelot.{builder.Environment.EnvironmentName}.json``.\r\n\r\n.. _config-merging-tomemory:\r\n\r\nMerging files to memory [#f2]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nBy default, Ocelot writes the merged configuration to disk as `ocelot.json`_ (the primary configuration file) by adding the file to the ASP.NET configuration provider.\r\n\r\nIf your web server lacks write permissions for the configuration folder, you can instruct Ocelot to use the merged configuration directly from memory.\r\nHere's how:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 5\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        // It implicitly calls ASP.NET AddJsonStream extension method for IConfigurationBuilder\r\n        // .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json)));\r\n        .AddOcelot(builder.Environment, MergeOcelotJson.ToMemory);\r\n\r\nThis feature proves exceptionally valuable in cloud environments like Azure, AWS, and GCP, especially when the app lacks sufficient write permissions to save files.\r\nFurthermore, within Docker container environments, permissions can be scarce, necessitating substantial DevOps efforts to enable file write operations.\r\nTherefore, save time by leveraging this feature!\r\n\r\nReload On Change\r\n----------------\r\n\r\nOcelot supports reloading the JSON configuration file on change.\r\nFor instance, the following will recreate Ocelot internal configuration when the `ocelot.json`_ file is updated manually:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddJsonFile(\"ocelot.json\", optional: false, reloadOnChange: true) // ASP.NET framework version\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note**: Starting from version `23.2`_, most :ref:`di-configuration-addocelot-methods` include optional ``bool?`` arguments, specifically ``optional`` and ``reloadOnChange``.\r\n  Therefore, you have the flexibility to provide these arguments when invoking the native `AddJsonFile method <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile?view=net-9.0-pp#microsoft-extensions-configuration-jsonconfigurationextensions-addjsonfile(microsoft-extensions-configuration-iconfigurationbuilder-system-string-system-boolean-system-boolean)>`_ during the final configuration step (see `AddOcelotJsonFile <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20AddOcelotJsonFile&type=code>`_ implementation).\r\n\r\nWe recommend using the :ref:`di-configuration-addocelot-methods` to control reloading, rather than relying on the framework's ``AddJsonFile`` method.\r\nFor example:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 4,13-16\r\n\r\n    // Old solution based on native framework functionality\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional: false, reloadOnChange: true);\r\n\r\n    var config = builder.Configuration;\r\n    var env = builder.Environment;\r\n    var mergeTo = MergeOcelotJson.ToFile; // ToMemory\r\n    var folder = \"/My/folder\";\r\n    var configuration = new FileConfiguration(); // read from anywhere and initialize\r\n\r\n    // Advanced solutions based on Ocelot functionality\r\n    config.AddOcelot(env, mergeTo, optional: false, reloadOnChange: true); // with environment and merging type\r\n    config.AddOcelot(folder, env, mergeTo, optional: false, reloadOnChange: true); // with folder, environment and merging type\r\n    config.AddOcelot(configuration, optional: false, reloadOnChange: true); // with configuration object created by your own\r\n    config.AddOcelot(configuration, env, mergeTo, optional: false, reloadOnChange: true); // with configuration object, environment and merging type\r\n\r\nExamining the code within the ``ConfigurationBuilderExtensions`` class would be helpful for gaining a better understanding of the signatures of the overloaded :ref:`di-configuration-addocelot-methods`.\r\n\r\n.. _config-react-to-changes:\r\n\r\nReact to Changes\r\n----------------\r\n\r\nResolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :ref:`administration-api` or `ocelot.json`_ being reloaded from the disk.\r\n\r\nYou may either poll the change token's ``IChangeToken.HasChanged`` property, or register a callback with the ``RegisterChangeCallback`` method.\r\n\r\n  **How to poll** is explained here:\r\n\r\n  .. code-block:: csharp\r\n\r\n      public class ConfigurationNotifyingService : BackgroundService\r\n      {\r\n          private readonly IOcelotConfigurationChangeTokenSource _tokenSource;\r\n          private readonly ILogger _logger;\r\n\r\n          public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger)\r\n          {\r\n              _tokenSource = tokenSource;\r\n              _logger = logger;\r\n          }\r\n\r\n          protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n          {\r\n              while (!stoppingToken.IsCancellationRequested)\r\n              {\r\n                  if (_tokenSource.ChangeToken.HasChanged)\r\n                  {\r\n                      _logger.LogInformation(\"Configuration has changed\");\r\n                  }\r\n                  await Task.Delay(1000, stoppingToken);\r\n              }\r\n          }\r\n      }\r\n\r\n  **How to register a callback** is explained here:\r\n\r\n  .. code-block:: csharp\r\n\r\n      public sealed class MyConfigurationNotifying : IDisposable\r\n      {\r\n          private readonly IOcelotConfigurationChangeTokenSource _tokenSource;\r\n          private readonly IDisposable _callbackHolder;\r\n\r\n          public MyConfigurationNotifying(IOcelotConfigurationChangeTokenSource tokenSource)\r\n          {\r\n              _tokenSource = tokenSource;\r\n              _callbackHolder = tokenSource.ChangeToken\r\n                  .RegisterChangeCallback(_ => Console.WriteLine(\"Configuration has changed\"), null);\r\n          }\r\n\r\n          public void Dispose() => _callbackHolder.Dispose();\r\n      }\r\n\r\nStore in `Consul`_\r\n------------------\r\n\r\nAs a developer, if you have enabled :doc:`../features/servicediscovery` with `Consul`_ support in Ocelot, you may choose to manage your configuration saving to the *Consul* `KV store`_.\r\n\r\nBeyond the traditional methods of storing configuration in a file vs folder (:ref:`config-merging-files`), or in-memory (:ref:`config-merging-tomemory`), you also have the alternative to utilize the `Consul`_ server's storage capabilities.\r\n\r\nFor further details on managing Ocelot configurations via a Consul instance, please consult the \":ref:`sd-consul-configuration-in-kv`\" section.\r\n\r\n.. _config-build-from-scratch:\r\n\r\nBuild From Scratch\r\n------------------\r\n\r\n  Class: `FileConfiguration <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileConfiguration.cs>`_\r\n\r\nStoring, reading, and writing static configurations may have limitations.\r\nTherefore, for more flexible and advanced scenarios the ``FileConfiguration`` object can be built from scratch in C# code of Ocelot application startup.\r\nAdditionally after reading static configuration from various sources such as, remote file systems, remote storages or cloudages, you can rewrite options to the configuration.\r\n\r\nOcelot does not provide a fluent syntax to build configuration on fly as other products do.\r\nHowever, it is possible to inject a ``FileConfiguration`` object during Ocelot startup using the :ref:`di-configuration-addocelot-methods` with a special parameter:\r\n\r\n.. code-block:: csharp\r\n\r\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, /* optional */);\r\n\r\nThe method above will deserialize the object to disk.\r\nIf you prefer to keep the configuration in memory, the following method includes the ``MergeOcelotJson`` parameter:\r\n\r\n.. code-block:: csharp\r\n\r\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, /* optional */);\r\n\r\nIn summary, the final .NET 8+ solution should be written in `Program`_ using `top-level statements <https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements>`_:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 8,13,14\r\n\r\n    using Ocelot.Configuration.File;\r\n    using Ocelot.DependencyInjection;\r\n    using Ocelot.Middleware;\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n\r\n    // Build Ocelot's configuration object on the fly:\r\n    var config = new FileConfiguration(); // create new or read static state from anywhere\r\n    // ... initialize or rewrite props: add routes, global config, etc.\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddOcelot(config) // MergeOcelotJson.ToFile : writing config JSON back to disk\r\n        .AddOcelot(config, builder.Environment, MergeOcelotJson.ToMemory); // merging to memory\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\n    var app = builder.Build();\r\n    await app.UseOcelot();\r\n    await app.RunAsync();\r\n\r\nAs a final step, you could add shutdown logic to save the complete configuration back to the storage, deserializing it to JSON format.\r\n\r\n.. _config-http-handler-options:\r\n\r\n``HttpHandlerOptions``\r\n----------------------\r\n\r\n  | Class: `FileHttpHandlerOptions <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs>`_\r\n  | MS Learn: `SocketsHttpHandler Class <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.socketshttphandler>`_\r\n\r\nThis route configuration section allows for following HTTP redirects, for instance, via the boolean ``AllowAutoRedirect`` option.\r\nThese options can be set at the route or global level for both static and :ref:`dynamic routing <routing-dynamic>`.\r\n\r\nUse ``HttpHandlerOptions`` in a route configuration to set up `HttpMessageHandler <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20HttpMessageHandler&type=code>`_ behavior\r\nbased on a `SocketsHttpHandler <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+SocketsHttpHandler&type=code>`_ instance:\r\n\r\n.. code-block:: json\r\n\r\n  \"HttpHandlerOptions\": {\r\n    \"AllowAutoRedirect\": false,\r\n    \"MaxConnectionsPerServer\": 2147483647, // max integer\r\n    \"PooledConnectionLifetimeSeconds\": 120,\r\n    \"UseCookieContainer\": false,\r\n    \"UseProxy\": false,\r\n    \"UseTracing\": false\r\n  }\r\n\r\n.. list-table::\r\n    :widths: 25 75\r\n    :header-rows: 1\r\n\r\n    * - *Option*\r\n      - *Description*\r\n    * - | ``AllowAutoRedirect``\r\n        | default: ``false``\r\n      - This value indicates whether the request should follow `Redirection messages <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages>`_ (HTTP 3xx status codes).\r\n        Set it ``true`` if the request should automatically follow redirection responses from the downstream resource; otherwise ``false``.\r\n    * - | ``MaxConnectionsPerServer``\r\n        | default: ``2147483647``, maximum integer\r\n      - This controls how many connections the internal ``HttpMessageInvoker`` will open to a single :ref:`hosting-gotchas-iis`/:ref:`hosting-gotchas-kestrel` server.\r\n    * - | ``PooledConnectionLifetimeSeconds``\r\n        | default: ``120`` seconds\r\n      - This controls how long a connection can be in the pool to be considered reusable.\r\n        Also refer to the **1st note** below!\r\n    * - | ``UseCookieContainer``\r\n        | default: ``false``\r\n      - This indicates whether the handler uses the ``CookieContainer`` property to store server cookies and uses these cookies when sending requests.\r\n        Also refer to the **2nd note** below!\r\n    * - | ``UseProxy``\r\n        | default: ``false``\r\n      - Refer to MS Learn: `UseProxy Property <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.socketshttphandler.useproxy>`_\r\n    * - | ``UseTracing``\r\n        | default: ``false``\r\n      - This enables :doc:`../features/tracing` feature in Ocelot.\r\n        Also refer to the **3rd note** below!\r\n\r\n.. note::\r\n\r\n  1. If the ``PooledConnectionLifetimeSeconds`` option is not defined, the default value is ``120`` seconds,\r\n  which is hardcoded in the `HttpHandlerOptions <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/HttpHandlerOptions.cs>`_ class as the ``DefaultPooledConnectionLifetimeSeconds`` constant.\r\n\r\n  2. If you use the ``CookieContainer``, Ocelot caches the ``HttpMessageInvoker`` for each downstream service.\r\n  This means that all requests to that downstream service will share the same cookies.\r\n  Issue `274 <https://github.com/ThreeMammals/Ocelot/issues/274>`_ was created because a user noticed that the cookies were being shared.\r\n  The Ocelot team tried to think of a nice way to handle this but we think it is impossible.\r\n  If you don't cache the clients, that means each request gets a new client and therefore a new cookie container.\r\n  If you clear the cookies from the cached client container, you get race conditions due to inflight requests. \r\n  This would also mean that subsequent requests don't use the cookies from the previous response!\r\n  All in all not a great situation.\r\n  We would avoid setting ``UseCookieContainer`` to ``true`` unless you have a really really good reason.\r\n  Just look at your response headers and forward the cookies back with your next request!\r\n\r\n  3. ``UseTracing`` option adds a tracing ``DelegatingHandler`` (aka ``Ocelot.Requester.ITracingHandler``) after obtaining it from ``ITracingHandlerFactory``,\r\n  encapsulating the ``Ocelot.Logging.IOcelotTracer`` service of DI-container.\r\n\r\n  4. Prior to version `24.1`_, global ``HttpHandlerOptions`` were not accessible, as they were only available at the route level for static routes.\r\n  Since version `24.1`_, global configuration is supported for both static and dynamic routes.\r\n\r\n.. _ssl-errors:\r\n\r\nSSL Errors\r\n----------\r\n\r\nIf you want to ignore SSL warnings (errors), set the following in your route configuration:\r\n\r\n.. code-block:: json\r\n\r\n    \"DangerousAcceptAnyServerCertificateValidator\": true\r\n\r\n**We don't recommend doing this!**\r\nThe team suggests creating your own certificate and then getting it trusted by your local (or remote) machine, if you can.\r\nFor ``https`` scheme, this fake validator was requested by issue `309 <https://github.com/ThreeMammals/Ocelot/issues/309>`_.\r\nFor ``wss`` scheme, this fake validator was added by PR `1377 <https://github.com/ThreeMammals/Ocelot/pull/1377>`_. \r\n\r\n  **Note**: As a team, we do not consider it an ideal solution.\r\n  On one hand, the community wants to have an option to work with self-signed certificates.\r\n  But on the other hand, currently, source code scanners detect two serious security vulnerabilities because of this fake validator in version `20.0`_ and higher.\r\n  The Ocelot team will rethink this unfortunate situation, and it is highly likely that this feature will at least be redesigned or removed completely.\r\n\r\nFor now, the SSL fake validator makes sense in local development environments when a route has ``https`` or ``wss`` schemes with self-signed certificates for those routes.\r\nThere are no other reasons to use the ``DangerousAcceptAnyServerCertificateValidator`` property at all!\r\n\r\nAs a team, we highly recommend following these instructions when developing your gateway app with Ocelot:\r\n\r\n* **Local development environments**: Use this feature to avoid SSL errors for self-signed certificates in the case of ``https`` or ``wss`` schemes.\r\n  We understand that some routes should have the downstream scheme exactly with SSL, because they are also in development and/or deployed using SSL protocols.\r\n  However, we believe that, especially for local development, you can switch from ``https`` to ``http`` without any objection since the services are in development and there is no risk of data leakage.\r\n\r\n* **Remote development environments**: Everything is the same as for local development.\r\n  However, this case is less strict; you have more options to use real certificates to switch off the feature.\r\n  For instance, you can deploy downstream services to cloud and hosting providers that have their own signed certificates for SSL.\r\n  At least your team can deploy one remote web server to host downstream services. Install your own certificate or use the cloud provider's one.\r\n\r\n* **Staging or testing environments**: We do not recommend using self-signed certificates because web servers should have valid certificates installed.\r\n  Ask your system administrator or DevOps engineers to create valid certificates.\r\n\r\n* **Production environments**: **Do not use self-signed certificates at all!**\r\n  System administrators or DevOps engineers must create real valid certificates signed by hosting or cloud providers.\r\n  **Switch off the feature for all routes!**\r\n  Remove the ``DangerousAcceptAnyServerCertificateValidator`` property for all routes in the production version of the `ocelot.json`_ file!\r\n\r\n.. _config-http-version:\r\n\r\n``DownstreamHttpVersion``\r\n-------------------------\r\n\r\n  MS Learn: `HttpVersion Class <https://learn.microsoft.com/en-us/dotnet/api/system.net.httpversion>`_\r\n\r\nOcelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1``, or ``2.0``.\r\n\r\n.. _config-version-policy:\r\n\r\n``DownstreamHttpVersionPolicy`` [#f3]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n  Enum: `HttpVersionPolicy <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpversionpolicy>`_\r\n\r\nThis routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests.\r\nFor additional details, refer to the following documentation:\r\n\r\n* `HttpRequestMessage.VersionPolicy Property <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy>`_\r\n* `HttpVersionPolicy Enum <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpversionpolicy>`_\r\n* `HttpVersion Class <https://learn.microsoft.com/en-us/dotnet/api/system.net.httpversion>`_\r\n\r\nThe ``DownstreamHttpVersionPolicy`` option is intricately linked with the :ref:`config-http-version` setting.\r\nTherefore, merely specifying ``DownstreamHttpVersion`` may sometimes be inadequate, particularly if your downstream services or Ocelot logs report HTTP connection errors such as ``PROTOCOL_ERROR``.\r\nIn these routes, selecting the precise ``DownstreamHttpVersionPolicy`` value is crucial for the ``HttpVersion`` policy to prevent such protocol errors.\r\n\r\nHTTP2 version policy\r\n^^^^^^^^^^^^^^^^^^^^\r\n\r\n**Given** you aim to ensure a smooth HTTP/2 connection setup for the Ocelot app and downstream services with SSL enabled:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"DownstreamScheme\": \"https\",\r\n    \"DownstreamHttpVersion\": \"2.0\",\r\n    \"DownstreamHttpVersionPolicy\": \"\", // empty or not defined\r\n    \"DangerousAcceptAnyServerCertificateValidator\": true\r\n  }\r\n\r\n**And** you configure global settings to use :ref:`hosting-gotchas-kestrel` with this snippet:\r\n\r\n.. code-block:: csharp\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n    builder.WebHost.ConfigureKestrel(serverOptions =>\r\n    {\r\n        serverOptions.ConfigureEndpointDefaults(listenOptions =>\r\n        {\r\n            listenOptions.Protocols = HttpProtocols.Http2;\r\n        });\r\n    });\r\n\r\n**When** all components are set to communicate exclusively via HTTP/2 without TLS (plain HTTP).\r\n\r\n**Then** the downstream services may display error messages such as:\r\n\r\n.. code-block::\r\n\r\n  HTTP/2 connection error (PROTOCOL_ERROR): Invalid HTTP/2 connection preface\r\n\r\nTo resolve the issue, ensure that ``HttpRequestMessage`` has its ``VersionPolicy`` set to ``RequestVersionOrHigher``.\r\nTherefore, the ``DownstreamHttpVersionPolicy`` should be defined as follows:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"DownstreamHttpVersion\": \"2.0\",\r\n    \"DownstreamHttpVersionPolicy\": \"RequestVersionOrHigher\" // !\r\n  }\r\n\r\nDependency Injection\r\n--------------------\r\n\r\n  Class: `ConfigurationBuilderExtensions`_\r\n\r\n*Dependency Injection* for this *Configuration* feature in Ocelot is designed to extend and/or control the configuration of the Ocelot Core before the stage of building ASP.NET Core pipeline services.\r\nThe primary methods are :ref:`di-configuration-addocelot-methods` within the ``ConfigurationBuilderExtensions`` class, which offers several overloaded versions with corresponding signatures.\r\nYou can utilize these methods in the `Program`_.cs file of your gateway app to configure the Ocelot pipeline and services.\r\n\r\nFind additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter.\r\n\r\n.. _config-route-metadata:\r\n\r\nExtend with ``Metadata``\r\n------------------------\r\n\r\n  Feature: :doc:`../features/metadata` [#f4]_\r\n\r\nThe ``Metadata`` options can store any arbitrary data that users can access in middlewares, delegating handlers, etc.\r\nBy using the *metadata*, users can implement their own logic and extend the functionality of Ocelot.\r\n\r\nThe :doc:`../features/metadata` feature is designed to extend both the static :ref:`config-route-schema` and :ref:`config-dynamic-route-schema`.\r\nGlobal *metadata* must be defined in the ``Metadata`` section, while parsing options should be placed in the ``MetadataOptions`` section.\r\n\r\nThe following example demonstrates practical usage of this feature:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 10,21-22\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        // other opts...\r\n        \"Metadata\": {\r\n          \"api-id\": \"FindPost\",\r\n          \"my-extension/param1\": \"overwritten-value\",\r\n          \"other-extension/param1\": \"value1\",\r\n          \"other-extension/param2\": \"value2\",\r\n          \"tags\": \"tag1, tag2, area1, area2, func1\",\r\n          \"json\": \"[1, 2, 3, 4, 5]\"\r\n        }\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      // other opts...\r\n      \"Metadata\": {\r\n        \"instance_name\": \"dc-1-54abcz\",\r\n        \"my-extension/param1\": \"default-value\"\r\n      },\r\n      \"MetadataOptions\": {\r\n        // parsing metadata opts...\r\n      }\r\n    }\r\n  }\r\n\r\n.. _break3: http://break.do\r\n\r\n  **Note**: Route *metadata* prevails over global *metadata* from the ``GlobalConfiguration`` section.\r\n  Therefore, if the same key data are defined both at the route and global levels, the route *metadata* overrides the global ones.\r\n\r\nNow, the route *metadata* can be accessed through the `DownstreamRoute <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+DownstreamRoute%28%29&type=code>`_ object:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 8,9\r\n\r\n  using Ocelot.Metadata;\r\n\r\n  public static class OcelotMiddlewares\r\n  {\r\n      public static Task PreAuthenticationMiddleware(HttpContext context, Func<Task> next)\r\n      {\r\n          var route = context.Items.DownstreamRoute();\r\n          var param1 = route.GetMetadata<string>(\"my-extension/param1\") ?? throw new ArgumentNullException(\"my-extension/param1\");\r\n          var param2 = route.GetMetadata<string>(\"other-extension/param2\", \"default-value\");\r\n          // Working with metadata...\r\n          return next();\r\n      }\r\n  }\r\n\r\nFor comprehensive documentation, please refer to the :doc:`../features/metadata` chapter.\r\n\r\n.. _config-timeout:\r\n\r\n``Timeout``\r\n-----------\r\n\r\nThis feature [#f5]_ is designed as part of the ``MessageInvokerPool``, which contains cached ``HttpMessageInvoker`` objects per route.\r\nEach created ``HttpMessageInvoker`` encapsulates an ``HttpMessageHandler``, specifically a ``SocketsHttpHandler`` instance, which serves as the base handler for the request pipeline.\r\nThis pipeline also includes all user-defined :doc:`../features/delegatinghandlers`.\r\nFinally, both the :doc:`../features/delegatinghandlers` and the base ``SocketsHttpHandler`` are wrapped by Ocelot's custom ``TimeoutDelegatingHandler``, which provides the internal timeout functionality.\r\n\r\n  **Note**: This design is subject to future review because ``TimeoutDelegatingHandler`` overrides/mimics the default timeout properties of ``SocketsHttpHandler``, as well as the behavior of ``HttpMessageInvoker`` as a controller for ``HttpMessageHandler`` objects.\r\n\r\nTo configure timeouts (in seconds) at different levels, choose the appropriate level and provide the corresponding JSON configuration.\r\n\r\nRoute timeout\r\n^^^^^^^^^^^^^\r\n\r\nA *route timeout* (also known as Requester middleware timeout based on ``TimeoutDelegatingHandler``) can be easily defined using the following JSON, according to the :ref:`config-route-schema`:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // upstream props\r\n    // downstream props\r\n    \"Timeout\": 3 // seconds\r\n  }\r\n\r\nPlease note that the route-level timeout takes precedence over the global timeout.\r\nThe same configuration applies to *dynamic routes*, according to the :ref:`config-dynamic-route-schema`.\r\n\r\nGlobal timeout\r\n^^^^^^^^^^^^^^\r\n\r\nA *global configuration timeout* can be defined using the following JSON, according to the :ref:`config-global-configuration-schema`:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // routes...\r\n    \"GlobalConfiguration\": {\r\n      // other props\r\n      \"Timeout\": 60 // seconds, 1 minute\r\n    }\r\n  }\r\n\r\nPlease note that the global timeout is substituted into a route if the route-level timeout is not defined, and it takes precedence over the absolute :ref:`config-default-timeout`.\r\nAdditionally, the global timeout may be omitted in the JSON configuration in favor of the absolute :ref:`config-default-timeout`, which is also configurable via a property of the C# static class.\r\n\r\nQoS timeout\r\n^^^^^^^^^^^\r\n\r\nA :doc:`../features/qualityofservice` (QoS) *timeout* can be defined using the :ref:`qos-schema` and the QoS :ref:`qos-timeout-strategy`:\r\n\r\n.. code-block:: json\r\n\r\n  \"QoSOptions\": {\r\n    \"Timeout\": 5000 // milliseconds\r\n  }\r\n\r\nPlease note, the *Quality of Service* timeout takes precedence over both route-level and global timeouts, which are ignored when QoS is enabled.\r\nAdditionally, avoid defining both *timeouts* in the same route, as the QoS timeout has higher priority than the route-level timeout.\r\nTherefore, the following route configuration **is not** recommended:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // route props...\r\n    \"Timeout\": 3, // seconds\r\n    \"QoSOptions\": {\r\n      \"Timeout\": 5000 // milliseconds\r\n    }\r\n  }\r\n\r\nSo, route ``Timeout`` will be ignored in favor of QoS ``Timeout``.\r\nMoreover, because the 3-second duration is shorter than 5000 milliseconds, you may observe warning messages in the logs that begin with the following sentence:\r\n\r\n.. code-block:: text\r\n\r\n  Route '/xxx' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: ...\r\n\r\nFor more details about this warning, refer to the :ref:`qos-notes-qos-and-route-global-timeouts` note in the :doc:`../features/qualityofservice` chapter.\r\nYour next recommended action is to completely remove the 3-second ``Timeout`` property or comment it out:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // \"Timeout\": 3, // seconds\r\n    \"QoSOptions\": {\r\n      \"Timeout\": 5000 // milliseconds\r\n    }\r\n  }\r\n\r\n.. note::\r\n\r\n  1. Both route ``Timeout`` and QoS ``Timeout`` are nullable positive integers, with a minimum valid value of ``1``.\r\n  Values in the range ``(−∞, 0]`` are treated as \"no value\" and will be automatically converted to the absolute :ref:`config-default-timeout`, effectively ignoring the property.\r\n\r\n  2. The unit of measurement for route ``Timeout`` is seconds,\r\n  whereas QoS ``Timeout`` is measured in milliseconds.\r\n\r\n.. _config-default-timeout:\r\n\r\nDefault timeout\r\n^^^^^^^^^^^^^^^\r\n.. _DefTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22const+int+DefTimeout%22&type=code\r\n.. _DefaultTimeoutSeconds: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22static+int+DefaultTimeoutSeconds%22&type=code\r\n\r\nTimeout values defined at different levels in the JSON configuration can serve as fallback defaults for other levels.\r\n\r\n- The absolute timeout (also known as ``DownstreamRoute`` `DefaultTimeoutSeconds`_) defaults to 90 seconds (as defined by the ``DownstreamRoute`` `DefTimeout`_ constant).\r\n  It acts as the default timeout when neither route-level nor global timeouts are defined.\r\n- The global configuration timeout, if not defined, also defaults to ``DownstreamRoute.DefaultTimeoutSeconds``.\r\n  If defined, it serves as the default timeout for all routes.\r\n- The Quality of Service (QoS) global timeout acts as the default timeout for all routes where QoS is enabled.\r\n\r\nTo configure the absolute timeout (currently 90 seconds, as defined by the ``DownstreamRoute`` `DefTimeout`_ constant),\r\nassign the desired number of seconds to the ``DownstreamRoute`` `DefaultTimeoutSeconds`_ static property in your `Program`_ class:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.Configuration;\r\n\r\n  DownstreamRoute.DefaultTimeoutSeconds = 3; // seconds, value must be >= 3\r\n\r\nHowever, keep in mind that the absolute timeout has the lowest priority—therefore, route-level and global timeouts will override this C# property if they are defined.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The \":ref:`Merging Files <config-merging-files>`\" feature was requested in issue `296`_, since then we extended it in issue `1216`_ (PR `1227`_) as \":ref:`Merging files to memory <config-merging-tomemory>`\" subfeature which was released as a part of version `23.2`_.\r\n.. [#f2] The \":ref:`Merging files to memory <config-merging-tomemory>`\" feature is based on the `MergeOcelotJson <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/MergeOcelotJson.cs>`_ enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class.\r\n.. [#f3] The \":ref:`DownstreamHttpVersionPolicy <config-version-policy>`\" feature was requested in issue `1672`_ as a part of version `23.3`_.\r\n.. [#f4] The \":ref:`config-route-metadata`\" feature was requested in issues `738`_ and `1990`_, and it was released as part of version `23.3`_.\r\n.. [#f5] The initial draft design of the :ref:`config-timeout` feature was implemented in pull request `1824`_ as ``TimeoutDelegatingHandler`` (released in version `23.0`_), but this version supported only the built-in `default timeout of 90 seconds`_.\r\n  The full :ref:`config-timeout` feature was requested in issue `1314`_, implemented in pull request `2073`_, and officially released as part of version `24.1`_.\r\n\r\n.. _default timeout of 90 seconds: https://github.com/ThreeMammals/Ocelot/blob/24.0.0/src/Ocelot/Requester/MessageInvokerPool.cs#L38\r\n.. _296: https://github.com/ThreeMammals/Ocelot/issues/296\r\n.. _585: https://github.com/ThreeMammals/Ocelot/issues/585\r\n.. _738: https://github.com/ThreeMammals/Ocelot/issues/738\r\n.. _1216: https://github.com/ThreeMammals/Ocelot/issues/1216\r\n.. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227\r\n.. _1314: https://github.com/ThreeMammals/Ocelot/issues/1314\r\n.. _1672: https://github.com/ThreeMammals/Ocelot/issues/1672\r\n.. _1824: https://github.com/ThreeMammals/Ocelot/pull/1824\r\n.. _1990: https://github.com/ThreeMammals/Ocelot/issues/1990\r\n.. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073\r\n\r\n.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0\r\n.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0\r\n.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13\r\n"
  },
  {
    "path": "docs/features/delegatinghandlers.rst",
    "content": ".. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n\r\nDelegating Handlers\r\n===================\r\n\r\n    **MS Learn Documentation:**\r\n\r\n    * `DelegatingHandler Class <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler>`_\r\n    * `HTTP Message Handlers in ASP.NET Web API <https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers>`_\r\n    * `HttpClient Message Handlers in ASP.NET Web API <https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/httpclient-message-handlers>`_\r\n\r\nOcelot allows the user to add `delegating handlers <https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler>`_ to the ``HttpClient`` transport. [#f1]_\r\n\r\nConfiguration\r\n-------------\r\n\r\nIn order to utilize the :doc:`../features/delegatinghandlers` feature, you need to do the following three steps of configuration.\r\n\r\n1. Create a class that can be used as a *delegating handler*: it must inherit from the ``DelegatingHandler`` class.\r\n   We are going to register these handlers in the ASP.NET Core DI container, so you can inject any other services you have registered into the constructor of your handler.\r\n\r\n   .. code-block:: csharp\r\n\r\n    public class MyHandler : DelegatingHandler\r\n    {\r\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)\r\n        {\r\n            // Do stuff before sending request, and optionally call the base handler...\r\n            var response = await base.SendAsync(request, token);\r\n            // Do post-processing of the response...\r\n            return response;\r\n        }\r\n    }\r\n\r\n2. You must add the handlers to the DI container in your `Program`_, as shown below:\r\n\r\n   .. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddDelegatingHandler<MyHandler>()\r\n        .AddDelegatingHandler<MyHandlerTwo>();\r\n\r\n   Both of these ``AddDelegatingHandler{T}`` methods have an optional parameter called ``global``, which is set to ``false``.\r\n   If it is ``false``, then the intent of the *delegating handler* is to be applied to specific routes via `ocelot.json`_ (see step 3).\r\n   If it is set to ``true``, then it becomes a global handler and will be applied to all routes, as shown below:\r\n\r\n   .. code-block:: csharp\r\n\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddDelegatingHandler<MyGlobalHandler>(true);  // it's global!\r\n\r\n.. _break: http://break.do\r\n\r\n    **Note 1**: The generic ``AddDelegatingHandler<T>(bool)`` method has another overloaded non-generic one with the ``Type`` parameter: ``AddDelegatingHandler(Type, bool)``.\r\n    Thus, here is an alternative to set it up:\r\n\r\n    .. code-block:: csharp\r\n\r\n        builder.Services\r\n            .AddOcelot(builder.Configuration)\r\n            .AddDelegatingHandler(typeof(MyHandler)) // for selected routes only\r\n            .AddDelegatingHandler(typeof(MyGlobalHandler), true); // it's global!\r\n\r\n    **Note 2**: Both versions of the methods add transient services to the DI container. It is recommended to utilize the generic version.\r\n\r\n3. If you want route-specific *delegating handlers* or to order your specific and/or global *delegating handlers* (more on this in the :ref:`dh-execution-order` section), then you must add the following to the specific route in `ocelot.json`_.\r\n   The names in the array must match the class names of your *delegating handlers* for Ocelot to match them together:\r\n\r\n   .. code-block:: json\r\n\r\n     \"DelegatingHandlers\": [ \"MyHandlerTwo\", \"MyHandler\" ]\r\n\r\n.. _dh-execution-order:\r\n\r\nExecution Order\r\n---------------\r\n\r\nYou can have as many *delegating handlers* as you want, and they are run in the following order:\r\n\r\n1. Any globals that are left in the order they were added to services and are not in the ``DelegatingHandlers`` option array from `ocelot.json`_.\r\n2. Any non-global *delegating handlers* plus any globals that were in the ``DelegatingHandlers`` option array from `ocelot.json`_, ordered as they are in the ``DelegatingHandlers`` array.\r\n3. Tracing *delegating handler*, if enabled (refer to the :doc:`../features/tracing` chapter).\r\n4. Quality of Service *delegating handler*, if enabled (refer to the :doc:`../features/qualityofservice` chapter).\r\n5. The ``HttpClient`` sends the ``HttpRequestMessage``.\r\n\r\nHopefully, other people will find this feature useful!\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] This feature was requested in issue `208`_, and the team decided that it would be useful in various ways, releasing it in version `3.0.3`_. Since then, we extended it in issue `264`_ and released it in version `5.0.0`_.\r\n\r\n.. _208: https://github.com/ThreeMammals/Ocelot/issues/208\r\n.. _264: https://github.com/ThreeMammals/Ocelot/issues/264\r\n\r\n.. _3.0.3: https://github.com/ThreeMammals/Ocelot/releases/tag/3.0.3\r\n.. _5.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.0.0\r\n"
  },
  {
    "path": "docs/features/dependencyinjection.rst",
    "content": ".. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\n.. _ServiceCollectionExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs\n.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs\n\nDependency Injection\n====================\n\n    | Namespace: ``Ocelot.DependencyInjection``\n    | Source code: `DependencyInjection <https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/DependencyInjection>`_\n\n.. _di-services-overview:\n\nServices Overview\n-----------------\n\n*Dependency Injection* feature in Ocelot is designed to extend and/or control the building of Ocelot Core as ASP.NET Core pipeline services.\nThe main methods of the `ServiceCollectionExtensions`_ class are:\n\n* The :ref:`di-services-addocelot-method` adds the required Ocelot services to the DI container and adds default services using the :ref:`di-adddefaultaspnetservices-method`.\n* The :ref:`di-addocelotusingbuilder-method` adds the required Ocelot services to the DI container and adds custom ASP.NET services with configuration injected implicitly or explicitly.\n\nUse :ref:`di-iservicecollection-extensions` in your `Program`_ (ASP.NET Core app) to add and build Ocelot Core services.\nThe fact is, the :ref:`di-ocelotbuilder-class` is Ocelot's cornerstone logic.\n\n.. _di-iservicecollection-extensions:\n\n``IServiceCollection`` extensions\n---------------------------------\n\n  Class: ``Ocelot.DependencyInjection.`` `ServiceCollectionExtensions`_\n\nBased on the current implementations for the :ref:`di-ocelotbuilder-class`, the :ref:`di-services-addocelot-method` adds the required ASP.NET services to the DI container.\nYou could call the more extended :ref:`di-addocelotusingbuilder-method` while configuring services to build and use a custom builder via an ``IMvcCoreBuilder`` object.\n\n.. _di-services-addocelot-method:\n\n``AddOcelot`` method\n^^^^^^^^^^^^^^^^^^^^\n\n**Signatures**:\n\n.. code-block:: csharp\n\n    IOcelotBuilder AddOcelot(this IServiceCollection services);\n    IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration);\n\nThese ``IServiceCollection`` extension methods add default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly.\n\n    **Note**: Both methods add the required and *default* ASP.NET Core services for Ocelot Core in the :ref:`di-adddefaultaspnetservices-method`, which is the default builder.\n\nIn this scenario, you do nothing other than call the ``AddOcelot`` method, which is often mentioned in feature chapters if additional startup settings are required.\nWith this method, you simply reuse the default settings to build the Ocelot Core. The alternative is the ``AddOcelotUsingBuilder`` method; see the next subsection.\n\n.. _di-addocelotusingbuilder-method:\n\n``AddOcelotUsingBuilder`` method\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Signatures**:\n\n.. code-block:: csharp\n\n    using CustomBuilderFunc = System.Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder>;\n\n    IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, CustomBuilderFunc customBuilder);\n    IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, CustomBuilderFunc customBuilder);\n\nThese ``IServiceCollection`` extension methods add Ocelot application services and **custom** ASP.NET Core services with configuration injected implicitly or explicitly.\n\n    **Note**: The method adds **custom** ASP.NET Core services required for Ocelot Core using a custom builder (aka ``customBuilder`` parameter).\n    It is highly recommended to read the documentation of the :ref:`di-adddefaultaspnetservices-method`,\n    or even review the implementation to understand the default ASP.NET Core services which are the minimal part of the gateway pipeline.\n\nIn this custom scenario, you control everything during the ASP.NET Core build process, and you provide custom settings to build Ocelot Core.\n\n.. _di-ocelotbuilder-class:\n\n``OcelotBuilder`` class\n-----------------------\n\nThe `OcelotBuilder <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/OcelotBuilder.cs>`_ class is the core of Ocelot which does the following:\n\n- Contructs itself by single public constructor:\n\n  .. code-block:: csharp\n\n    public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder = null);\n\n- Initializes and stores public properties: ``Services`` (of ``IServiceCollection`` type), ``Configuration`` (of ``IConfiguration`` type), and ``MvcCoreBuilder`` (of ``IMvcCoreBuilder`` type).\n- Adds *all application services* during the construction phase via the ``Services`` property.\n- Adds ASP.NET Core services by builder using ``Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder>`` object in these 2 development scenarios:\n- Adds ASP.NET Core services by builder using a ``Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder>`` object in these two development scenarios:\n\n    1. By default builder (:ref:`di-adddefaultaspnetservices-method`) if there is no ``customBuilder`` parameter provided.\n    2. By :ref:`di-custom-builder` with the provided delegate object as the ``customBuilder`` parameter.\n\n- Adds (switches on/off) Ocelot features through the following methods:\n\n  * ``AddSingletonDefinedAggregator`` and ``AddTransientDefinedAggregator`` methods\n  * ``AddCustomLoadBalancer`` method\n  * ``AddDelegatingHandler`` method\n  * ``AddConfigPlaceholders`` method\n\n.. _di-adddefaultaspnetservices-method:\n\n``AddDefaultAspNetServices`` method\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    Part of the :ref:`di-ocelotbuilder-class`\n\nCurrently, the method is protected, and overriding is forbidden.\nThe role of the method is to inject the required services via both the ``IServiceCollection`` and ``IMvcCoreBuilder`` interface objects for the minimal part of the gateway pipeline.\n\nCurrent `implementation <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+AddDefaultAspNetServices+language%3AC%23&type=code&l=C%23>`_ is the folowing:\n\n.. code-block:: csharp\n\n    protected IMvcCoreBuilder AddDefaultAspNetServices(IMvcCoreBuilder builder, Assembly assembly)\n    {\n        Services\n            .AddLogging()\n            .AddMiddlewareAnalysis()\n            .AddWebEncoders();\n        return builder\n            .AddApplicationPart(assembly)\n            .AddControllersAsServices()\n            .AddAuthorization()\n            .AddNewtonsoftJson();\n    }\n\nThe method cannot be overridden. It is not virtual, and there is no way to override the current behavior by inheritance.\nThe method is the default builder of Ocelot Core when calling the :ref:`di-services-addocelot-method`.\nAs an alternative, to \"override\" this default builder, you can design and reuse a custom builder as a ``Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder>`` delegate object and pass it as a parameter to the :ref:`di-addocelotusingbuilder-method`.\nIt gives you full control over the design and building of Ocelot Core, but be careful when designing your custom Ocelot pipeline as a customizable ASP.NET Core pipeline.\n\n    **Warning**: Most of the services from the minimal part of the pipeline should be reused, but only a few services can be removed.\n\n    **Warning**: The method above is called after adding the required services of the ASP.NET Core pipeline by the `AddMvcCore <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoreservicecollectionextensions.addmvccore>`_ method via the ``Services`` property in the upper calling context.\n    These services are the absolute minimum core services for the ASP.NET MVC pipeline.\n    They must always be added to the DI container and are added implicitly before calling the method by the caller in the upper context.\n    So, ``AddMvcCore`` creates an ``IMvcCoreBuilder`` object and assigns it to the ``MvcCoreBuilder`` property.\n    Finally, as a default builder, the method above receives the ``IMvcCoreBuilder`` object, making it ready for further extensions.\n\nThe next section shows you an example of designing a custom Ocelot Core using a custom builder.\n\n.. _di-custom-builder:\n\nCustom Builder\n--------------\n\n**Goal**: Replace ``Newtonsoft.Json`` services with ``System.Text.Json`` services.\n\nProblem\n^^^^^^^\n\nThe main :ref:`di-services-addocelot-method` adds `Newtonsoft JSON <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.newtonsoftjsonmvccorebuilderextensions.addnewtonsoftjson>`_ services\nusing the ``AddNewtonsoftJson`` extension method in the default builder (:ref:`di-adddefaultaspnetservices-method`).\nThe ``AddNewtonsoftJson`` method was introduced in earlier .NET and Ocelot releases, which was necessary before Microsoft launched the ``System.Text.Json`` library.\nHowever, it now affects normal use, so we intend to solve the problem.\n\nModern `JSON services <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoremvccorebuilderextensions.addjsonoptions>`_ \nout of `the box <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoremvccorebuilderextensions>`_\nwill help configure JSON settings using the ``JsonSerializerOptions`` property for JSON formatters during (de)serialization.\n\nSolution\n^^^^^^^^\n\nWe have the following methods in `ServiceCollectionExtensions`_ class:\n\n.. code-block:: csharp\n\n    IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder);\n    IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder);\n\nThese methods with a custom builder allow you to use any desired JSON library for (de)serialization.\nHowever, we are going to create a custom ``MvcCoreBuilder`` with support for JSON services, such as ``System.Text.Json``.\nTo do that, we need to call the ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreBuilderExtensions`` class\n(NuGet `Microsoft.AspNetCore.Mvc.Core <https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core/>`_ package) in `Program`_:\n\n.. code-block:: csharp\n  :emphasize-lines: 6\n\n    builder.Services\n        .AddLogging()\n        .AddMiddlewareAnalysis()\n        .AddWebEncoders()\n        // Add your custom builder\n        .AddOcelotUsingBuilder(builder.Configuration, MyCustomBuilder);\n\n    static IMvcCoreBuilder MyCustomBuilder(IMvcCoreBuilder builder, Assembly assembly) => builder\n        .AddApplicationPart(assembly)\n        .AddControllersAsServices()\n        .AddAuthorization()\n        // Replace AddNewtonsoftJson() by AddJsonOptions()\n        .AddJsonOptions(options =>\n        {\n            options.JsonSerializerOptions.WriteIndented = true; // use System.Text.Json\n        });\n\nThe sample code provides settings to render JSON as indented text rather than as compressed plain JSON text without spaces.\nThis is just one common use case, and you can add additional services to the builder.\n\n---------------------------------------------------------------------------------------------------------------------------\n\n.. _di-configuration-overview:\n\nConfiguration Overview\n----------------------\n\n*Dependency Injection* for the :doc:`../features/configuration` feature in Ocelot is designed to extend and set up the configuration of the Ocelot Core **before** the stage of building ASP.NET Core services (see :ref:`di-services-overview`).\nTo configure the Ocelot Core services, use the :ref:`di-configuration-extensions` in your `Program`_ of your gateway app.\n\n.. _di-configuration-extensions:\n\n``IConfigurationBuilder`` extensions\n------------------------------------\n\n  Class: ``Ocelot.DependencyInjection.`` `ConfigurationBuilderExtensions`_\n\nThe main methods are the :ref:`di-configuration-addocelot-methods` within the `ConfigurationBuilderExtensions`_ class.\nThese methods have a list of overloaded versions with corresponding signatures.\n\nThe purpose of the ``AddOcelot`` method is to prepare everything before actually configuring with native extensions.\nIt involves the following steps:\n\n1. **Merging Partial JSON Files**: The ``GetMergedOcelotJson`` method merges partial JSON files.\n2. **Selecting Merge Type**: It allows you to choose a merge type to save the merged JSON configuration data either ``ToFile`` or ``ToMemory``.\n3. **Framework Extensions**: Finally, the method calls the following native ``IConfigurationBuilder`` framework extensions:\n\n  * The ``AddJsonFile`` method adds the primary configuration file (commonly known as `ocelot.json`_) after the merge stage. It writes the file back *to the file system* using the ``ToFile`` merge type option, which is implicitly the default.\n  * The ``AddJsonStream`` method adds the JSON data of the primary configuration file as a UTF-8 stream *into memory* after the merge stage. It uses the ``ToMemory`` merge type option.\n\n.. _di-configuration-addocelot-methods:\n\n``AddOcelot`` methods\n^^^^^^^^^^^^^^^^^^^^^\n\n**Signatures** of the most common versions:\n\n.. code-block:: csharp\n\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env);\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env);\n\n.. _break: http://break.do\n\n    **Note**: These versions use the implicit ``ToFile`` merge type to write `ocelot.json`_ back to disk. Finally, they call the ``AddJsonFile`` extension.\n\n**Signatures** of the versions to specify a ``MergeOcelotJson`` option:\n\n.. code-block:: csharp\n\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null);\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null);\n\n.. _break2: http://break.do\n\n    **Note**: These versions include optional arguments to specify the location of the three main files involved in the merge operation.\n    In theory, these files can be located anywhere, but in practice, it is better to keep them in one folder.\n\n**Signatures** of the versions to indicate the ``FileConfiguration`` object of a self-created out-of-the-box configuration: [#f1]_\n\n.. code-block:: csharp\n\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration,\n        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null);\n    IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null);\n\n.. _break3: http://break.do\n\n    **Note 1**: These versions include optional arguments to specify the location of the three main files involved in the merge operation.\n\n    **Note 2**: Your ``FileConfiguration`` object can be serialized/deserialized from anywhere: local or remote storage, Consul KV storage, and even a database.\n    For more information about this super useful feature, please read PR `1569`_.\n\n\"\"\"\"\n\n.. [#f1] The :ref:`config-build-from-scratch` feature was requested in issues `1228`_ and `1235`_. It was delivered by PR `1569`_ as part of version `20.0`_. Since then, we have extended it in PR `1227`_ and released it as part of version `23.2`_.\n\n.. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227\n.. _1228: https://github.com/ThreeMammals/Ocelot/issues/1228\n.. _1235: https://github.com/ThreeMammals/Ocelot/issues/1235\n.. _1569: https://github.com/ThreeMammals/Ocelot/pull/1569\n.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0\n.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0\n"
  },
  {
    "path": "docs/features/errorcodes.rst",
    "content": "Error Handling\r\n==============\r\n.. _Handle errors in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling\r\n.. _standard error handling: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling\r\n\r\n  MS Learn: `Handle errors in ASP.NET Core`_\r\n\r\nOcelot has custom error handling for ``Exception`` objects.\r\nThus, we override the `standard error handling`_ provided by ASP.NET Core, which is based on manipulating ``Exception`` objects.\r\n\r\n.. _eh-middleware:\r\n\r\nMiddleware\r\n----------\r\n.. _499 Client Closed Request: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status499clientclosedrequest\r\n.. _500 Internal Server Error: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500\r\n\r\n  Class: `ExceptionHandlerMiddleware <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs>`_\r\n\r\nThe ``ExceptionHandlerMiddleware`` produces the following status codes, in fallback order, after setting the :ref:`lg-request-id`:\r\n\r\n1. Native response status: Returned when no exception is present, or when a mapped error status is available (excluding ``499`` and ``500``).\r\n2. `499 Client Closed Request`_: A custom Ocelot status returned when an ``OperationCanceledException`` occurs due to an aborted request.\r\n   A warning is logged.\r\n3. `500 Internal Server Error`_: The standard status returned when a generic ``Exception`` occurs and Ocelot does not process or map the error.\r\n   An error record is logged.\r\n\r\nOcelot returns HTTP status codes based on internal logic in specific cases of :ref:`eh-client-error-responses` and :ref:`eh-server-error-responses`.\r\n\r\n.. _eh-client-error-responses:\r\n\r\nClient Error Responses\r\n----------------------\r\n.. _401 Unauthorized: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401\r\n.. _403 Forbidden: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403\r\n.. _404 Not Found: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/404\r\n.. _RequestCanceledError: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+RequestCanceledError&type=code\r\n.. _OcelotErrorCode.RequestCanceled: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20OcelotErrorCode.RequestCanceled&type=code\r\n\r\n- `401 Unauthorized`_: If the authentication middleware runs and the user is not authenticated.\r\n- `403 Forbidden`_: If the authorization middleware runs and the user is unauthorized, if the claim value is not authorized, if the scope is not authorized, if the user does not have the required claim, or if the claim cannot be found.\r\n- `404 Not Found`_: If a downstream route cannot be found, or if Ocelot is unable to map an internal error code to an HTTP status code.\r\n- `499 Client Closed Request`_: If the request is canceled by the client.\r\n\r\n    | Ocelot Error: `RequestCanceledError <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+RequestCanceledError&type=code>`_\r\n    | Ocelot Code: `OcelotErrorCode.RequestCanceled <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20OcelotErrorCode.RequestCanceled&type=code>`_\r\n\r\n  According to Ocelot Core's design, HTTP status code ``499`` is returned in the following ``OperationCanceledException`` scenarios:\r\n\r\n  1. By ``ExceptionHandlerMiddleware``, if an ``OperationCanceledException`` is thrown and the context's cancellation token is in the \"cancellation requested\" state.\r\n     Ocelot logs a warning with the exception body. If the response has not started, the status code will be set to ``499``.\r\n  2. By ``ResponderMiddleware``, if the default ``IErrorsToHttpStatusCodeMapper`` service maps the detected `OcelotErrorCode.RequestCanceled`_ to status ``499``.\r\n     This error code is produced by the ``IExceptionToErrorMapper`` service when an ``OperationCanceledException`` is thrown by other middlewares.\r\n\r\n.. _eh-server-error-responses:\r\n\r\nServer Error Responses\r\n----------------------\r\n.. _502 Bad Gateway: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/502\r\n.. _503 Service Unavailable: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503\r\n\r\n- `500 Internal Server Error`_: If unable to complete the HTTP request to the downstream service, and the exception is not ``OperationCanceledException`` or ``HttpRequestException``.\r\n- `502 Bad Gateway`_: If unable to connect to the downstream service.\r\n- `503 Service Unavailable`_: Returned when the downstream request times out.\r\n\r\n    | Ocelot Error: `RequestTimedOutError <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+RequestTimedOutError&type=code>`_\r\n    | Ocelot Code: `OcelotErrorCode.RequestTimedOutError <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20OcelotErrorCode.RequestTimedOutError&type=code>`_\r\n\r\n  According to Ocelot Core's design, status code ``503`` is produced in the following ``TimeoutException`` scenarios:\r\n\r\n  1. By ``TimeoutDelegatingHandler`` from the ``IMessageInvokerPool`` service, when an ``OperationCanceledException`` is thrown and the context's cancellation token is not in the “cancellation requested” state.\r\n     Ocelot does not log an error with the exception body, but the ``IExceptionToErrorMapper`` service generates the internal `OcelotErrorCode.RequestTimedOutError`_.\r\n  2. By ``ResponderMiddleware``, if the default ``IErrorsToHttpStatusCodeMapper`` service maps the detected `OcelotErrorCode.RequestTimedOutError`_ to status ``503``.\r\n     This error code is produced by the ``IExceptionToErrorMapper`` service when a ``TimeoutException`` is thrown by other middlewares—especially by ``TimeoutDelegatingHandler``.\r\n\r\n.. _eh-error-mapper:\r\n\r\nError Mapper\r\n------------\r\n\r\n  Class: `HttpExceptionToErrorMapper <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs>`_\r\n\r\nHistorically, Ocelot errors are implemented by the `Exception-to-Error mapper <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20HttpExceptionToErrorMapper&type=code>`_.\r\nThe ``Map`` method converts an ``Exception`` object to a native ``Ocelot.Errors.Error`` object.\r\n\r\nWe override HTTP status codes because of ``Exception``-to-``Error`` mapping.\r\nThis can be confusing for the developer since the actual status code of the downstream service may be different and get lost.\r\nPlease research and review all response headers of the upstream service.\r\nIf you do not find status codes and/or required headers, then the :doc:`../features/headerstransformation` feature should help.\r\n\r\nWe expect you to share your use case with us in the `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ space of the repository. |octocat|\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n"
  },
  {
    "path": "docs/features/graphql.rst",
    "content": ".. _GraphQL: https://graphql.org/\r\n.. _Ocelot.Samples.GraphQL: https://github.com/ThreeMammals/Ocelot/tree/main/samples/GraphQL\r\n.. _graphql-dotnet: https://github.com/graphql-dotnet/graphql-dotnet\r\n.. |GraphQL Logo| image:: https://avatars.githubusercontent.com/u/13958777\r\n  :alt: GraphQL Logo\r\n  :width: 40\r\n\r\n|GraphQL Logo| GraphQL\r\n======================\r\n\r\nOcelot does not directly support `GraphQL`_, but many people have asked about it.\r\nWe wanted to show how easy it is to integrate the `GraphQL for .NET <https://github.com/graphql-dotnet/graphql-dotnet>`_ library.\r\n\r\nSample\r\n------\r\n\r\n  **Sample**: `Ocelot.Samples.GraphQL`_\r\n\r\nPlease see the sample project `Ocelot.Samples.GraphQL`_.\r\nUsing a combination of the `graphql-dotnet`_ project and Ocelot :doc:`../features/delegatinghandlers` feature, this is pretty easy to do.\r\nHowever, we do not intend to integrate more closely with `GraphQL`_ at the moment.\r\nCheck out the sample's `README.md <https://github.com/ThreeMammals/Ocelot/blob/main/samples/GraphQL/README.md>`_ for detailed instructions on how to do this.\r\n\r\nFuture\r\n------\r\n\r\nIf you have sufficient experience with `GraphQL`_ and the mentioned .NET `graphql-dotnet`_ package, we would welcome your contribution to the sample. |octocat|\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n\r\nWho knows, maybe you will get inspired by the sample development and come up with a design solution in the form of a rough draft of a *GraphQL* feature to implement in Ocelot.\r\nGood luck!\r\nAnd welcome to the `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ space of the repository!\r\n"
  },
  {
    "path": "docs/features/headerstransformation.rst",
    "content": ".. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n\r\nHeaders Transformation\r\n======================\r\n\r\nOcelot allows the user to transform `HTTP headers <https://developer.mozilla.org/en-US/docs/Glossary/HTTP_header>`_ both before and after the downstream request.\r\n\r\n  **Note**: *Headers Transformation* is generally available for static routes with a global configuration.\r\n  For dynamic and aggregate routes, this feature is not implemented. This limitation is noted in the current :ref:`ht-roadmap`.\r\n\r\nSchema\r\n------\r\n\r\nAs you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-schema` section, the route's *Headers Transformation* schema is quite simple, a JSON dictionary:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    // \"header_name\": \"transformation_expression\",\r\n  },\r\n  \"UpstreamHeaderTransform\": {\r\n    // \"header_name\": \"transformation_expression\",\r\n  },  \r\n\r\nTypically, a ``transformation_expression`` is a constant header value, a single placeholder from the :ref:`ht-placeholders` list, or a \":ref:`Find and Replace <ht-find-and-replace>`\" expression.\r\nAdditionally, the :ref:`config-global-configuration-schema` allows configuring global *Headers Transformations* (refer to the :ref:`Configuration <ht-configuration>` section).\r\n\r\n.. _ht-configuration:\r\n\r\nConfiguration [#f1]_\r\n--------------------\r\n\r\nA complete *configuration* consists of both route-level and global *Headers Transformations*.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        \"DownstreamHeaderTransform\": {\r\n          // ...\r\n        },\r\n        \"UpstreamHeaderTransform\": {\r\n          // ...\r\n        }\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"DownstreamHeaderTransform\": {\r\n        // ...\r\n      },\r\n      \"UpstreamHeaderTransform\": {\r\n        // ...\r\n      }\r\n    }\r\n  }\r\n\r\n.. _break: http://break.do\r\n.. _Merge: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22public+static+IEnumerable%3CHeader%3E+Merge%22&type=code\r\n\r\n  **Note**: Route-level transformations take precedence over global transformations.\r\n  In addition, when route-level transformations are defined, they do not entirely override the full set of header names from the global configuration.\r\n  Ocelot's Core internal `Merge`_ algorithm identifies global header names not specified at the route level and appends them to the route's header set.\r\n\r\n.. _ht-find-and-replace:\r\n\r\nFind and Replace [#f2]_\r\n-----------------------\r\n\r\nIn order to transform a header first we specify the header key and then the type of transform we want e.g.\r\n\r\n.. code-block:: json\r\n\r\n  \"Test\": \"http://www.bbc.co.uk/, http://ocelot.net/\"\r\n\r\nThe key is ``Test`` and the value is ``http://www.bbc.co.uk/, http://ocelot.net/``.\r\nThe value is saying: replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``.\r\nThe syntax is ``{find}, {replace}``. Hopefully pretty simple. There are examples below that explain more.\r\n\r\n**Pre Downstream Request**\r\n\r\nAdd the following to a Route in `ocelot.json`_ in order to replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``.\r\nThis header will be changed before the request downstream and will be sent to the downstream server.\r\n\r\n.. code-block:: json\r\n\r\n  \"UpstreamHeaderTransform\": {\r\n    \"Test\": \"http://www.bbc.co.uk/, http://ocelot.net/\"\r\n  }\r\n\r\n**Post Downstream Request**\r\n\r\nAdd the following to a Route in `ocelot.json`_ in order to replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``.\r\nThis transformation will take place after Ocelot has received the response from the downstream service.\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"Test\": \"http://www.bbc.co.uk/, http://ocelot.net/\"\r\n  }\r\n\r\n.. _ht-add-to-request:\r\n\r\nAdd to Request [#f3]_\r\n---------------------\r\n\r\nIf you want to add a header to your upstream request please add the following to a route in your `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n\r\n  \"UpstreamHeaderTransform\": {\r\n    \"Uncle\": \"Bob\"\r\n  }\r\n\r\nIn the example above a header with the key ``Uncle`` and value ``Bob`` would be send to to the upstream service.\r\n\r\n  :ref:`ht-placeholders` are supported too (see below).\r\n\r\n.. _ht-add-to-response:\r\n\r\nAdd to Response [#f4]_\r\n----------------------\r\n\r\nIf you want to add a header to your downstream response, please add the following to a route in `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"Uncle\": \"Bob\"\r\n  }\r\n\r\nIn the example above a header with the key ``Uncle`` and value ``Bob`` would be returned by Ocelot when requesting the specific route.\r\n\r\nIf you want to return the :ref:`tr-butterfly` Trace ID, do something like the following:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"AnyKey\": \"{TraceId}\"\r\n  }\r\n\r\n.. _ht-placeholders:\r\n\r\nPlaceholders\r\n------------\r\n\r\nOcelot allows placeholders that can be used in header transformation.\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - *Placeholder*\r\n    - *Description*\r\n  * - ``{BaseUrl}``\r\n    - This will use Ocelot base URL e.g. ``http://localhost:5000`` as its value.\r\n  * - ``{DownstreamBaseUrl}``\r\n    - This will use the downstream services base URL e.g. ``http://localhost:5000`` as its value. This only works for ``DownstreamHeaderTransform`` route option at the moment.\r\n  * - ``{RemoteIpAddress}``\r\n    - This will find the clients IP address using ``HttpContext.Connection.RemoteIpAddress``, so you will get back some IP. See more in the `GetRemoteIpAddress <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20%22Response%3Cstring%3E%20GetRemoteIpAddress()%22&type=code>`_ method.\r\n  * - ``{TraceId}``\r\n    - This will use the :ref:`tr-butterfly` Trace ID. This only works for ``DownstreamHeaderTransform`` route option at the moment.\r\n  * - ``{UpstreamHost}``\r\n    - This will look for the incoming ``Host`` header.\r\n\r\nFor now, we believe these placeholders are sufficient for basic user scenarios.\r\nHowever, if you need additional placeholders, refer to the :ref:`ht-roadmap`.\r\n\r\nSamples\r\n-------\r\n\r\nHandling 302 redirects\r\n^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nOcelot will by default automatically follow redirects, however if you want to return the location header to the client, you might want to change the location to be Ocelot not the downstream service.\r\nOcelot allows this with the following configuration:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"Location\": \"http://www.bbc.co.uk/, http://ocelot.net/\"\r\n  },\r\n  \"HttpHandlerOptions\": {\r\n    \"AllowAutoRedirect\": false,\r\n  }\r\n\r\nOr, you could use the ``{BaseUrl}`` placeholder.\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"Location\": \"http://localhost:6773, {BaseUrl}\"\r\n  },\r\n  \"HttpHandlerOptions\": {\r\n    \"AllowAutoRedirect\": false,\r\n  }\r\n\r\nFinally, if you are using a load balancer with Ocelot, you will get multiple downstream base URLs so the above would not work.\r\nIn this case you can do the following:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamHeaderTransform\": {\r\n    \"Location\": \"{DownstreamBaseUrl}, {BaseUrl}\"\r\n  },\r\n  \"HttpHandlerOptions\": {\r\n    \"AllowAutoRedirect\": false,\r\n  }\r\n\r\n``X-Forwarded-For`` header\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nAn example of using ``{RemoteIpAddress}`` placeholder:\r\n\r\n.. code-block:: json\r\n\r\n  \"UpstreamHeaderTransform\": {\r\n    \"X-Forwarded-For\": \"{RemoteIpAddress}\"\r\n  }\r\n\r\n.. _ht-roadmap:\r\n\r\nRoadmap\r\n-------\r\n\r\n1. Ideally the \":ref:`Find and Replace <ht-find-and-replace>`\" feature would be able to support the fact that a header can have multiple values.\r\n   At the moment it just assumes one.\r\n   It would also be nice if it could multi find and replace e.g. \r\n\r\n   .. code-block:: json\r\n\r\n    \"DownstreamHeaderTransform\": {\r\n      \"Location\": \"[{one,one},{two,two}]\"\r\n    },\r\n    \"HttpHandlerOptions\": {\r\n      \"AllowAutoRedirect\": false,\r\n    }\r\n\r\n.. _break2: http://break.do\r\n.. _moderate effort: https://github.com/ThreeMammals/Ocelot/labels/medium%20effort\r\n.. _significant effort: https://github.com/ThreeMammals/Ocelot/labels/large%20effort\r\n\r\n2. The *Headers Transformation* feature is not implemented for :ref:`Dynamic Routes <config-dynamic-route-schema>` and :ref:`Aggregate Routes <config-aggregate-route-schema>`.\r\n   For :ref:`Dynamic Routing <routing-dynamic>`, potential development would require `moderate effort`_.\r\n   However, the Ocelot team expects that designing and implementing *Headers Transformation* for :doc:`../features/aggregation` will demand `significant effort`_, as aggregated routes typically lose their headers.\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n\r\nIdeas and proposals are welcome in the repository's `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ space. |octocat|\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The global :ref:`Configuration <ht-configuration>` feature was requested in issue `1658`_ and released in version `24.1`_.\r\n.. [#f2] The \":ref:`Find and Replace <ht-find-and-replace>`\" feature was requested in issue `190`_, initially released in version `2.0.11`_, and the team decided that it would be useful in various ways.\r\n.. [#f3] The \":ref:`Add to Request <ht-add-to-request>`\" feature was requested in issue `313`_ and released in version `5.5.3`_.\r\n.. [#f4] The \":ref:`Add to Response <ht-add-to-response>`\" feature was requested in issue `280`_ and released in version `5.1.0`_.\r\n\r\n.. _2.0.11: https://github.com/ThreeMammals/Ocelot/releases/tag/2.0.11\r\n.. _5.1.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.1.0\r\n.. _5.5.3: https://github.com/ThreeMammals/Ocelot/releases/tag/5.5.3\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _190: https://github.com/ThreeMammals/Ocelot/issues/190\r\n.. _280: https://github.com/ThreeMammals/Ocelot/issues/280\r\n.. _313: https://github.com/ThreeMammals/Ocelot/issues/313\r\n.. _1658: https://github.com/ThreeMammals/Ocelot/issues/1658\r\n"
  },
  {
    "path": "docs/features/kubernetes.rst",
    "content": ".. role:: htm(raw)\r\n  :format: html\r\n.. role:: pdf(raw)\r\n  :format: latex pdflatex\r\n.. |K8sLogo| image:: https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo.png\r\n  :alt: K8s Logo\r\n  :height: 50\r\n  :class: img-valign-bottom\r\n  :target: https://kubernetes.io\r\n.. |logo-kubernetes| image:: ../images/k8s-logo-kubernetes.png\r\n  :alt: kubernetes logo\r\n  :height: 30\r\n  :class: img-valign-middle\r\n  :target: https://kubernetes.io\r\n\r\n.. _KubeClient: https://www.nuget.org/packages/KubeClient\r\n.. _Ocelot.Provider.Kubernetes: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes\r\n.. _package: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes\r\n\r\n|K8sLogo| Kubernetes (K8s) [#f1]_\r\n=================================\r\n\r\n    | Feature of: :doc:`../features/servicediscovery`\r\n    | Quick Links: `K8s Website <https://kubernetes.io/>`_ | `K8s Documentation <https://kubernetes.io/docs/>`_ | `K8s GitHub <https://github.com/kubernetes/kubernetes>`_\r\n\r\nOcelot will call the `K8s <https://kubernetes.io/>`_ endpoints API in a given namespace to get all of the endpoints for a pod and then load balance across them.\r\nOcelot used to use the services API to send requests to the `K8s`_ service but this was changed in pull request `1134`_ because the service did not load balance as expected.\r\n\r\nOur NuGet `Ocelot.Provider.Kubernetes`_ extension package is based on the `KubeClient`_ package.\r\nFor a comprehensive understanding, it is essential refer to the `KubeClient`_ documentation.\r\n\r\n.. _k8s-install:\r\n\r\nInstall\r\n-------\r\n\r\nThe first thing you need to do is install the `package`_ that provides |logo-kubernetes| support in Ocelot:\r\n\r\n.. code-block:: powershell\r\n\r\n    Install-Package Ocelot.Provider.Kubernetes\r\n\r\n``AddKubernetes(bool)`` method\r\n------------------------------\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n  public static class OcelotBuilderExtensions\r\n  {\r\n      public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true);\r\n  }\r\n\r\nThis extension-method adds `K8s`_ services **with** or **without** using a pod service account.\r\nThen add the following to your `Program <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Kubernetes/ApiGateway/Program.cs>`_:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddKubernetes(); // usePodServiceAccount is true\r\n\r\nIf you have services deployed in Kubernetes, you will normally use the naming service to access them.\r\n\r\n1. By default the ``useServiceAccount`` argument is true, which means that Service Account using Pod to access the service of the `K8s`_ cluster needs to be Service Account based on RBAC authorization:\r\n\r\n   You can replicate a Permissive using RBAC role bindings (see `Permissive RBAC Permissions <https://kubernetes.io/docs/reference/access-authn-authz/rbac/#permissive-rbac-permissions>`_),\r\n   `K8s`_ API server and token will read from pod.\r\n\r\n   .. code-block:: bash\r\n\r\n     kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --user=admin --user=kubelet --group=system:serviceaccounts\r\n\r\n   Finally, it creates the `KubeClient`_ from pod service account.\r\n\r\n2. When the ``useServiceAccount`` argument is false, you need to provide `KubeClientOptions <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20KubeClientOptions&type=code>`_ to create `KubeClient`_ using them.\r\n   You have to bind the options configuration section for the DI ``IOptions<KubeClientOptions>`` interface or register a custom action to initialize the options:\r\n\r\n   .. code-block:: csharp\r\n    :emphasize-lines: 9, 10, 13\r\n\r\n     Action<KubeClientOptions> configureKubeClient = opts => \r\n     { \r\n         opts.ApiEndPoint = new UriBuilder(\"https\", \"my-host\", 443).Uri;\r\n         opts.AccessToken = \"my-token\";\r\n         opts.AuthStrategy = KubeAuthStrategy.BearerToken;\r\n         opts.AllowInsecure = true; \r\n     };\r\n     builder.Services\r\n         .AddOptions<KubeClientOptions>()\r\n         .Configure(configureKubeClient); // manual binding options via IOptions<KubeClientOptions>\r\n     builder.Services\r\n         .AddOcelot(builder.Configuration)\r\n         .AddKubernetes(false); // don't use pod service account, and IOptions<KubeClientOptions> is reused\r\n\r\n   .. _break: http://break.do\r\n\r\n      **Note**, this could also be written like this (shortened version):\r\n\r\n      .. code-block:: csharp\r\n        :emphasize-lines: 2, 10\r\n\r\n        builder.Services\r\n            .AddKubeClientOptions(opts =>\r\n            {\r\n                opts.ApiEndPoint = new UriBuilder(\"https\", \"my-host\", 443).Uri;\r\n                opts.AuthStrategy = KubeAuthStrategy.BearerToken;\r\n                opts.AccessToken = \"my-token\";\r\n                opts.AllowInsecure = true;\r\n            })\r\n            .AddOcelot(builder.Configuration)\r\n            .AddKubernetes(false); // don't use pod service account, and client options provided via AddKubeClientOptions\r\n\r\n   Finally, it creates the `KubeClient`_ from your options.\r\n\r\n    **Note 1**: For understanding the ``IOptions<TOptions>`` interface, please refer to the Microsoft Learn documentation: `Options pattern in .NET <https://learn.microsoft.com/en-us/dotnet/core/extensions/options>`_.\r\n\r\n    **Note 2**: Please consider this Case 2 as an example of manual setup when you **do not** use a pod service account.\r\n    We recommend using our official extension method, which receives an ``Action<KubeClientOptions>`` argument with your options: refer to the :ref:`k8s-addkubernetes-action-method` below.\r\n\r\n.. _k8s-addkubernetes-action-method:\r\n\r\n``AddKubernetes(Action<KubeClientOptions>)`` method [#f2]_\r\n----------------------------------------------------------\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 3\r\n\r\n  public static class OcelotBuilderExtensions\r\n  {\r\n      public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, Action<KubeClientOptions> configureOptions, /*optional params*/);\r\n  }\r\n\r\nThis extension method adds `K8s`_ services **without** using a pod service account, explicitly calling an action to initialize configuration options for `KubeClient`_.\r\nIt operates in two modes:\r\n\r\n1. If ``configureOptions`` is provided (action is not null), it calls the action, ignoring all optional arguments.\r\n\r\n   .. code-block:: csharp\r\n    :emphasize-lines: 8\r\n\r\n    Action<KubeClientOptions> configureKubeClient = opts => \r\n    {\r\n        opts.ApiEndPoint = new UriBuilder(\"https\", \"my-host\", 443).Uri;\r\n        // ...\r\n    };\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddKubernetes(configureKubeClient); // without optional arguments\r\n\r\n.. _break: http://break.do\r\n\r\n     **Note**: Optional arguments do not make sense; all settings are defined inside the ``configureKubeClient`` action.\r\n\r\n2. If ``configureOptions`` is not provided (action is null), it reads the global ``ServiceDiscoveryProvider`` :ref:`k8s-configuration` options and reuses them to initialize the following properties:\r\n   ``ApiEndPoint``, ``AccessToken``, and ``KubeNamespace``, finally initializing the rest of the properties with optional arguments.\r\n\r\n   .. code-block:: csharp\r\n    :emphasize-lines: 3, 5\r\n\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddKubernetes(null, allowInsecure: true, /*optional args*/) // shortened version\r\n        // or\r\n        .AddKubernetes(configureOptions: null, allowInsecure: true, /*optional args*/); // long version\r\n\r\n.. _break2: http://break.do\r\n\r\n     **Note**: Optional arguments must be used here in addition to the options coming from the global ``ServiceDiscoveryProvider`` :ref:`k8s-configuration`.\r\n     Find the comprehensive documentation in the C# code of the `AddKubernetes <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22public+static+IOcelotBuilder+AddKubernetes%28this+IOcelotBuilder+builder%2C%22+language%3AC%23&type=code>`_ methods.\r\n\r\n.. _k8s-configuration:\r\n\r\nConfiguration\r\n-------------\r\n\r\nThe following examples show how to set up a route that will work in Kubernetes.\r\nThe most important thing is the ``ServiceName`` which is made up of the Kubernetes service name.\r\nWe also need to set up the ``ServiceDiscoveryProvider`` in ``GlobalConfiguration``.\r\n\r\nRegarding global and route configurations, if your downstream service resides in a different namespace, you can override the global setting at the route level by specifying a ``ServiceNamespace``.\r\n\r\n.. code-block:: json\r\n\r\n  \"Routes\": [\r\n    {\r\n      \"ServiceName\": \"my-service\",\r\n      \"ServiceNamespace\": \"my-namespace\"\r\n    }\r\n  ]\r\n\r\n.. _k8s-kube-provider:\r\n\r\n``Kube`` provider\r\n-----------------\r\n\r\nThe example here shows a typical configuration:\r\n\r\n.. code-block:: json\r\n\r\n  \"Routes\": [\r\n    {\r\n      \"ServiceName\": \"my-service\",\r\n      // ...\r\n    }\r\n  ],\r\n  \"GlobalConfiguration\": {\r\n    \"ServiceDiscoveryProvider\": {\r\n      \"Scheme\": \"https\",\r\n      \"Host\": \"my-host\",\r\n      \"Port\": 443,\r\n      \"Token\": \"my-token\",\r\n      \"Namespace\": \"Dev\",\r\n      \"Type\": \"Kube\"\r\n    }\r\n  }\r\n\r\nService deployment in ``Dev`` namespace, and discovery provider type is ``Kube``, you also can set :ref:`PollKube <k8s-pollkube-provider>` or :ref:`WatchKube <k8s-watchkube-provider>` provider type.\r\n\r\n  **Note 1**: ``Scheme``, ``Host``, ``Port``, and ``Token`` are not used if ``usePodServiceAccount`` is true when `KubeClient`_ is created from a pod service account.\r\n  Please refer to the :ref:`k8s-install` section for technical details.\r\n\r\n  **Note 2**: The ``Kube`` provider searches for the service entry using ``ServiceName`` and then retrieves the first available port from the ``EndpointSubsetV1.Ports`` collection.\r\n  Therefore, if the port name is not specified, the default downstream scheme will be ``http``; \r\n  Please refer to the \":ref:`Downstream Scheme vs Port Names <k8s-downstream-scheme-vs-port-names>`\" section for technical details.\r\n\r\n.. _k8s-pollkube-provider:\r\n\r\n``PollKube`` provider [#f3]_\r\n----------------------------\r\n\r\nYou use Ocelot to poll Kubernetes for latest service information rather than per request.\r\nIf you want to poll Kubernetes for the latest services rather than per request (default behaviour of the :ref:`k8s-kube-provider`) then you need to set the following configuration:\r\n\r\n.. code-block:: json\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Namespace\": \"dev\",\r\n    \"Type\": \"PollKube\",\r\n    \"PollingInterval\": 100 // ms\r\n  } \r\n\r\nThe polling interval is in milliseconds and tells Ocelot how often to call Kubernetes for changes in service configuration.\r\n\r\n  **Note**, there are tradeoffs here.\r\n  If you poll Kubernetes, it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request.\r\n  This really depends on how volatile your services are.\r\n  We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request.\r\n  There is no way for Ocelot to work these out for you, except perhaps through a `discussion <https://github.com/ThreeMammals/Ocelot/discussions>`_. \r\n\r\n.. _k8s-watchkube-provider:\r\n\r\n``WatchKube`` provider [#f4]_\r\n-----------------------------\r\n.. _Kubernetes API: https://kubernetes.io/docs/reference/using-api/\r\n.. _watch requests: https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes\r\n\r\nWith this configuration, `Kubernetes API`_ \"`watch requests`_\" are used to fetch service configuration.\r\nEssentially, it establishes one streamed HTTP connection with the `Kubernetes API`_ per downstream service.\r\nChanges streamed through this connection will be used to update the list of available endpoints.\r\n\r\n.. code-block:: json\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Namespace\": \"dev\",\r\n    \"Type\": \"WatchKube\"\r\n  }\r\n\r\n.. note::\r\n\r\n  The ``WatchKube`` provider is specifically designed for high-load Ocelot vs. Kubernetes environments with high RPS ratios.\r\n  To better understand which type is suitable for your needs, we have added a table :ref:`k8s-comparing-providers`.\r\n\r\nThe provider has an implicit configuration for fine-tuned watching, which are available and can only be initialized in C# code.\r\n\r\n* ``WatchKube.FirstResultsFetchingTimeoutSeconds``: `This <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FirstResultsFetchingTimeoutSeconds&type=code>`_ is the default number of seconds to wait after Ocelot starts, following the provider's creation, to fetch the first result from the Kubernetes endpoint. :sup:`1`\r\n* ``WatchKube.FailedSubscriptionRetrySeconds``: `This <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FailedSubscriptionRetrySeconds&type=code>`__ is the default number of seconds to wait before scheduling the next retry for the subscription operation. :sup:`1`\r\n\r\n.. _break3: http://break.do\r\n\r\n  :sup:`1` For both ``static int`` properties, the default value is 1 (one) second. The constraint ensures that the assigned value is greater than or equal to 1 (one). Therefore, the minimum value is 1 (one) second.\r\n  \r\n.. _k8s-comparing-providers:\r\n\r\nComparing providers\r\n-------------------\r\nThis table explains the most important indicators that may influence Ocelot vs. Kubernetes deployment or DevOps strategy.\r\nThe evolution path of all providers follows: ``Kube`` -> ``PollKube`` -> ``WatchKube``, with ``WatchKube`` being the most advanced provider.\r\n\r\n.. list-table::\r\n  :widths: 34 22 22 22\r\n  :header-rows: 1\r\n\r\n  * - *Indicators \\\\ Providers*\r\n    - :ref:`Kube <k8s-kube-provider>`\r\n    - :ref:`PollKube <k8s-pollkube-provider>`\r\n    - :ref:`WatchKube <k8s-watchkube-provider>`\r\n  * - Extra latency\r\n    - One hop per route\r\n    - \\-\r\n    - \\-\r\n  * - Speed of response to endpoints changes\r\n    - High\r\n    - Low :sup:`1`\r\n    - High\r\n  * - Pressure on `Kubernetes API`_\r\n    - High\r\n    - Low :sup:`1`\r\n    - Low\r\n  * - Ocelot load (estimated) :sup:`2`\r\n    - < 1000 RPS\r\n    - > 1000 RPS\r\n    - > 5000 RPS\r\n  * - Ocelot deployment :sup:`3`\r\n    - Single instance\r\n    - Multiple instances\r\n    - Cluster of instances\r\n\r\n.. _break4: http://break.do\r\n\r\n  | :sup:`1` Depends on the ``PollingInterval`` option.\r\n  | :sup:`2` Please consider this a rough load estimation, as our team has not provided any tests or benchmarks.\r\n  | :sup:`3` The term \"instance\" refers to an Ocelot instance, not a Kubernetes one.\r\n\r\n.. _k8s-downstream-scheme-vs-port-names:\r\n\r\nDownstream Scheme vs Port Names [#f5]_\r\n--------------------------------------\r\n\r\nKubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset.\r\nWhen binding multiple ports, you assign a name to each subset port.\r\nTo allow the ``Kube`` provider to recognize the desired port by its name, you need to specify the ``DownstreamScheme`` with the port's name;\r\nif not, the collection's first port entry will be chosen by default.\r\n\r\nFor instance, consider a service on Kubernetes that exposes two ports: ``https`` for 443 and ``http`` for 80, as follows:\r\n\r\n.. code-block:: text\r\n\r\n  Name:         my-service\r\n  Namespace:    default\r\n  Subsets:\r\n    Addresses:  10.1.161.59\r\n    Ports:\r\n      Name   Port  Protocol\r\n      ----   ----  --------\r\n      https  443   TCP\r\n      http   80    TCP\r\n\r\n**When** you need to use the ``http`` port while intentionally bypassing the default ``https`` port (first one),\r\nyou must define ``DownstreamScheme`` to enable the provider to recognize the desired ``http`` port by comparing ``DownstreamScheme`` with the port name as follows:\r\n\r\n.. code-block:: json\r\n\r\n  \"Routes\": [\r\n    {\r\n      \"ServiceName\": \"my-service\",\r\n      \"DownstreamScheme\": \"http\", // port name -> http -> port is 80\r\n    }\r\n  ]\r\n\r\n.. note::\r\n\r\n  In the absence of a specified ``DownstreamScheme`` (the default behavior), the :ref:`k8s-kube-provider`—as well as other providers—will select *the first available port* from the ``EndpointSubsetV1.Ports`` collection.\r\n  Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The \":doc:`../features/kubernetes`\" feature was requested as part of issue `345`_ to add support for `Kubernetes <https://kubernetes.io/>`_ :doc:`../features/servicediscovery` provider, and released in version `13.5.0`_ \r\n.. [#f2] The \":ref:`AddKubernetes(Action{KubeClientOptions}) method <k8s-addkubernetes-action-method>`\" was requested as part of issue `2255`_ (pull request `2257`_), and released in version `24.0`_\r\n.. [#f3] The evolution of the \":ref:`PollKube provider <k8s-pollkube-provider>`\" began with pull request `772`_ (version `13.2.0`_).\r\n  Since then, the provider's design was reviewed due to reported bug `2304`_, and patch `2335`_ was applied and rolled out in version `24.1`_.\r\n.. [#f4] The \":ref:`WatchKube provider <k8s-watchkube-provider>`\" was first discussed in thread `2168`_, later implemented in pull request `2174`_, and released in version `24.1`_.\r\n.. [#f5] The \":ref:`Downstream Scheme vs Port Names <k8s-downstream-scheme-vs-port-names>`\" feature was requested as part of issue `1967`_ and released in version `23.3`_\r\n\r\n.. _345: https://github.com/ThreeMammals/Ocelot/issues/345\r\n.. _772: https://github.com/ThreeMammals/Ocelot/pull/772\r\n.. _1134: https://github.com/ThreeMammals/Ocelot/pull/1134\r\n.. _1967: https://github.com/ThreeMammals/Ocelot/issues/1967\r\n.. _2168: https://github.com/ThreeMammals/Ocelot/discussions/2168\r\n.. _2174: https://github.com/ThreeMammals/Ocelot/pull/2174\r\n.. _2255: https://github.com/ThreeMammals/Ocelot/issues/2255\r\n.. _2257: https://github.com/ThreeMammals/Ocelot/pull/2257\r\n.. _2304: https://github.com/ThreeMammals/Ocelot/issues/2304\r\n.. _2335: https://github.com/ThreeMammals/Ocelot/pull/2335\r\n.. _13.2.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.2.0\r\n.. _13.5.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.0\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n"
  },
  {
    "path": "docs/features/loadbalancer.rst",
    "content": ".. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n\r\nLoad Balancer\r\n=============\r\n\r\nOcelot can load balance across available downstream services for each route.\r\nThis means you can scale your downstream services, and Ocelot can use them effectively.\r\n\r\n``LoadBalancerOptions`` Schema\r\n------------------------------\r\n\r\n.. _FileLoadBalancerOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs\r\n\r\n  Class: `FileLoadBalancerOptions`_\r\n\r\nThe following is the full *load balancer* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`.\r\nNot all of these options need to be configured; however, the ``Type`` option is mandatory.\r\n\r\n.. code-block:: json\r\n\r\n  \"LoadBalancerOptions\": {\r\n    \"Type\": \"\",\r\n    \"Key\": \"\", // CookieStickySessions balancer\r\n    \"Expiry\": 1 // ms, CookieStickySessions balancer\r\n  }\r\n\r\n.. list-table::\r\n  :widths: 15 85\r\n  :header-rows: 1\r\n\r\n  * - *Option*\r\n    - *Description*\r\n  * - ``Type``\r\n    - An in-built *load balancer* type selected from the list of available :ref:`lb-balancers`, or a user-defined type (refer to the \":ref:`Custom Balancers <lb-custom-balancers>`\" section).\r\n  * - ``Key``\r\n    - The name of the cookie you wish to use for sticky sessions. This option is applicable only to the :ref:`CookieStickySessions type <lb-cookiestickysessions-type>`.\r\n  * - ``Expiry``\r\n    - Expiration period specifies how long, in milliseconds, the session should remain sticky.\r\n      This value refreshes with each request to mimic typical session behavior. Note: This option applies only to the :ref:`CookieStickySessions type <lb-cookiestickysessions-type>`.\r\n\r\nThe actual ``LoadBalancerOptions`` schema with all the properties can be found in the C# `FileLoadBalancerOptions`_ class.\r\n\r\n.. _lb-configuration:\r\n\r\nConfiguration\r\n-------------\r\n\r\nThe following shows how to set up multiple downstream services for a static route using `ocelot.json`_ and then select the ``LeastConnection`` *load balancer*.\r\nThis is the simplest way to configure load balancing without using service discovery.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 10-12\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/posts/{postId}\",\r\n    \"UpstreamHttpMethod\": [ \"Put\", \"Delete\" ],\r\n    \"DownstreamPathTemplate\": \"/api/posts/{postId}\",\r\n    \"DownstreamScheme\": \"https\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"10.0.1.10\", \"Port\": 5000 },\r\n      { \"Host\": \"10.0.1.11\", \"Port\": 5000 }\r\n    ],\r\n    \"LoadBalancerOptions\": {\r\n      \"Type\": \"LeastConnection\"\r\n    }\r\n  }\r\n\r\nThe following shows how to set up a route using :doc:`../features/servicediscovery` and then select the ``RoundRobin`` *load balancer*.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // ...\r\n    \"ServiceName\": \"product\",\r\n    \"LoadBalancerOptions\": {\r\n      \"Type\": \"RoundRobin\"\r\n    }\r\n  }\r\n\r\nWhen this is set up, Ocelot will look up the downstream host and port from the :doc:`../features/servicediscovery` provider and load balance requests across any available services.\r\nIf you add and remove services from the :doc:`../features/servicediscovery` provider [#f1]_,\r\nOcelot should respect this and stop calling services that have been removed and start calling services that have been added.\r\n\r\n.. _lb-global-configuration:\r\n\r\nGlobal Configuration [#f2]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nA complete configuration consists of both route-level and global *load balancing*.\r\nYou can configure the following options in the ``GlobalConfiguration`` section of `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 4-8, 12, 17-20\r\n\r\n  \"Routes\": [\r\n    {\r\n      \"Key\": \"R0\", // optional\r\n      \"LoadBalancerOptions\": {\r\n        \"Type\": \"CookieStickySessions\",\r\n        \"Key\": \".AspNetCore.Session\",\r\n        \"Expiry\": 1200000 // milliseconds, 20 minutes\r\n      }\r\n    },\r\n    {\r\n      \"Key\": \"R1\", // this route is part of a group\r\n      \"LoadBalancerOptions\": {} // optional due to grouping\r\n    }\r\n  ],\r\n  \"GlobalConfiguration\": {\r\n    \"BaseUrl\": \"https://ocelot.net\",\r\n    \"LoadBalancerOptions\": {\r\n      \"RouteKeys\": [\"R1\"], // if undefined or empty array, opts will apply to all routes\r\n      \"Type\": \"LeastConnection\"\r\n    }\r\n  }\r\n\r\n:doc:`../features/servicediscovery` dynamic routes intentionally override the global :ref:`dynamic routing <sd-dynamic-routing>` configuration:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 5-7, 16-19\r\n\r\n  \"DynamicRoutes\": [\r\n    {\r\n      \"Key\": \"\", // optional\r\n      \"ServiceName\": \"my-service\",\r\n      \"LoadBalancerOptions\": {\r\n        \"Type\": \"LeastConnection\" // switch from RoundRobin to LeastConnection\r\n      }\r\n    }\r\n  ],\r\n  \"GlobalConfiguration\": {\r\n    \"BaseUrl\": \"https://ocelot.net\",\r\n    \"DownstreamScheme\": \"http\",\r\n    \"ServiceDiscoveryProvider\": {\r\n      // required section for dynamic routing\r\n    },\r\n    \"LoadBalancerOptions\": {\r\n      \"RouteKeys\": [], // no grouping, thus opts apply to all dynamic routes\r\n      \"Type\": \"RoundRobin\"\r\n    }\r\n  }\r\n\r\nIn this configuration, the ``RoundRobin`` balancer is used for all implicit dynamic routes.\r\nHowever, for the \"my-service\" service, the load balancer type has been explicitly switched from ``RoundRobin`` to ``LeastConnection``.\r\n\r\n.. note::\r\n\r\n  1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``LoadBalancerOptions``, the global options will apply to all routes.\r\n  If the array contains route keys, it defines a single group of routes to which the global options apply.\r\n  Routes excluded from this group must specify their own route-level ``LoadBalancerOptions``.\r\n\r\n  2. Prior to version `24.1`_, global ``LoadBalancerOptions`` were only accessible in the special :ref:`Dynamic Routing <routing-dynamic>` mode.\r\n  Since version `24.1`_, global configuration has been available for both static and dynamic routes.\r\n  As a team, we would consider the idea of implementing such a global configuration for aggregated routes.\r\n  However, an aggregated route is essentially a combination of static routes.\r\n\r\n.. _lb-balancers:\r\n\r\nBalancers\r\n---------\r\n\r\nThe available types of built-in *load balancers* are:\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - *Type*\r\n    - *Description*\r\n  * - ``CookieStickySessions``\r\n    - This uses a cookie to stick all requests to a specific server. More information can be found in the \":ref:`CookieStickySessions Type<lb-cookiestickysessions-type>`\" section.\r\n  * - ``LeastConnection``\r\n    - This tracks which services are dealing with requests and sends new requests to the service with the fewest (\"least\") existing requests. The algorithm state is not distributed across a cluster of Ocelots.\r\n  * - ``RoundRobin``\r\n    - This loops through available services and sends requests. The algorithm state is not distributed across a cluster of Ocelots.\r\n  * - ``NoLoadBalancer``\r\n    - This takes the first available service from :ref:`configuration <lb-configuration>` or :doc:`../features/servicediscovery` provider.\r\n\r\nYou must choose which *load balancer* to use in your :ref:`configuration <lb-configuration>`.\r\n\r\n.. _lb-cookiestickysessions-type:\r\n\r\n``CookieStickySessions`` Type [#f3]_\r\n------------------------------------\r\n\r\nWe have implemented a basic sticky session type of *load balancer*.\r\nThe scenario it is meant to support involves having a number of downstream servers that do not share session state.\r\nIf you receive more than one request for one of these servers, it should go to the same server each time; otherwise, the session state might be incorrect for the given user.\r\n\r\nIn order to set up the ``CookieStickySessions`` *load balancer*, you need to do something like the following:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/posts/{postId}\",\r\n    \"UpstreamHttpMethod\": [ \"Put\", \"Delete\" ],\r\n    \"DownstreamPathTemplate\": \"/api/posts/{postId}\",\r\n    \"DownstreamScheme\": \"https\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"10.0.1.10\", \"Port\": 5000 },\r\n      { \"Host\": \"10.0.1.11\", \"Port\": 5000 }\r\n    ],\r\n    \"LoadBalancerOptions\": {\r\n      \"Type\": \"CookieStickySessions\",\r\n      \"Key\": \".AspNetCore.Session\",\r\n      \"Expiry\": 1200000 // milliseconds, 20 minutes\r\n    }\r\n  }\r\n\r\nThese ``LoadBalancerOptions`` configure the ``CookieStickySessions`` load balancer using the standard session cookie ``Key`` for ASP.NET Core apps with sessions enabled.\r\nThe default expiration time is 20 minutes, matching the default session timeout in ASP.NET Core.\r\n\r\n  **Note 1**: If you have multiple routes with the same ``LoadBalancerOptions``, then all of those routes will use the same *load balancer* for their subsequent requests.\r\n  This means the sessions will be stuck across routes.\r\n\r\n  **Note 2**: If you define more than one ``DownstreamHostAndPort``, or if you are using a :doc:`../features/servicediscovery` provider such as :ref:`sd-consul` and it returns more than one service, then ``CookieStickySessions`` uses ``RoundRobin`` to select the next server.\r\n  This is hard-coded at the moment but could be changed.\r\n\r\n.. _lb-custom-balancers:\r\n\r\nCustom Balancers [#f4]_\r\n-----------------------\r\n\r\nIn order to create and use a custom *load balancer*, you can do the following.\r\nBelow, we set up a basic load balancing configuration, and note that the ``Type`` is ``MyLoadBalancer``, which is the name of a class we will set up to perform load balancing.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // ...\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"10.0.1.10\", \"Port\": 5000 },\r\n      { \"Host\": \"10.0.1.11\", \"Port\": 5000 }\r\n    ],\r\n    \"LoadBalancerOptions\": {\r\n      \"Type\": \"MyLoadBalancer\"\r\n    }\r\n  }\r\n\r\nThen, you need to create a class that implements the ``ILoadBalancer`` interface. Below is a simple round-robin example:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.LoadBalancer.LoadBalancers;\r\n  using Ocelot.Responses;\r\n  using Ocelot.Values;\r\n\r\n  public class MyLoadBalancer : ILoadBalancer\r\n  {\r\n      private readonly Func<Task<List<Service>>> _services;\r\n      private static object Locker = new();\r\n      private int _last;\r\n\r\n      public MyLoadBalancer() { }\r\n      public MyLoadBalancer(Func<Task<List<Service>>> services)\r\n          => _services = services;\r\n\r\n      public string Type => nameof(MyLoadBalancer);\r\n      public void Release(ServiceHostAndPort hostAndPort) { }\r\n\r\n      public async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext context)\r\n      {\r\n          var services = await _services.Invoke();\r\n          lock (Locker)\r\n          {\r\n              _last = (_last >= services.Count) ? 0 : _last;\r\n              var next = services[_last++];\r\n              return new OkResponse<ServiceHostAndPort>(next.HostAndPort);\r\n          }\r\n      }\r\n  }\r\n\r\nFinally, you need to register this class with Ocelot.\r\nWe have used the most complex example below to show all of the data and types that can be passed into the factory that creates *load balancers*.\r\n\r\n.. code-block:: csharp\r\n\r\n    using Ocelot.Configuration;\r\n    using Ocelot.DependencyInjection;\r\n    using Ocelot.ServiceDiscovery.Providers;\r\n\r\n    Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, MyLoadBalancer> lbFactory\r\n        = (serviceProvider, Route, discoveryProvider) => new MyLoadBalancer(discoveryProvider.GetAsync);\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration)\r\n        .AddCustomLoadBalancer(lbFactory);\r\n\r\nHowever, there is a much simpler example that will work the same way:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.DependencyInjection;\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddCustomLoadBalancer<MyLoadBalancer>();\r\n\r\n.. note::\r\n\r\n  1. There are numerous ``IOcelotBuilder`` `methods <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22IOcelotBuilder+AddCustomLoadBalancer%3CT%3E%28%22+language%3AC%23&type=code>`_ to add a custom *load balancer*.\r\n  The interface is as follows:\r\n\r\n  .. code-block:: csharp\r\n\r\n      IOcelotBuilder AddCustomLoadBalancer<T>()\r\n          where T : ILoadBalancer, new();\r\n      IOcelotBuilder AddCustomLoadBalancer<T>(Func<T> loadBalancerFactoryFunc)\r\n          where T : ILoadBalancer;\r\n      IOcelotBuilder AddCustomLoadBalancer<T>(Func<IServiceProvider, T> loadBalancerFactoryFunc)\r\n          where T : ILoadBalancer;\r\n      IOcelotBuilder AddCustomLoadBalancer<T>(Func<DownstreamRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)\r\n          where T : ILoadBalancer;\r\n      IOcelotBuilder AddCustomLoadBalancer<T>(Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)\r\n          where T : ILoadBalancer;\r\n\r\n  2. When you enable custom *load balancers*, Ocelot looks up your *load balancer* by its class name when it decides whether to perform load balancing.\r\n\r\n  * If it finds a match, it will use your load balancer to load balance.\r\n  * If Ocelot cannot match the *load balancer* type in your configuration with the name of the registered *load balancer* class, then you will receive an HTTP `500 Internal Server Error`_.\r\n  * If your *load balancer* factory throws an exception when Ocelot calls it, you will receive an HTTP `500 Internal Server Error`_.\r\n\r\n.. warning::\r\n\r\n  Remember, if you specify no *load balancer* in your :ref:`lb-configuration`, Ocelot will not attempt to load balance.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] Currently supported :doc:`../features/servicediscovery` providers are :ref:`sd-consul`, :doc:`Kubernetes <../features/kubernetes>`, :ref:`Eureka <sd-eureka>`, :doc:`../features/servicefabric`, and manually developed :ref:`sd-custom-providers`.\r\n.. [#f2] The \":ref:`Global Configuration <lb-global-configuration>`\" feature, as part of issue `585`_, was introduced in pull request `2324`_ and released in version `24.1`_.\r\n.. [#f3] The \":ref:`CookieStickySessions Type <lb-cookiestickysessions-type>`\" feature was requested in issue `322`_, though what the user wants is more complicated than just sticky sessions. Anyway, we thought this would be a nice feature to have! Initially, the feature was released in version `6.0.0`_.\r\n.. [#f4] The \":ref:`Custom Balancers <lb-custom-balancers>`\" feature by `David Lievrouw`_ implemented a way to provide Ocelot with a custom *load balancer* in pull request `1155`_ (issue `961`_, released in version `15.0.3`_).\r\n\r\n.. _322: https://github.com/ThreeMammals/Ocelot/issues/322\r\n.. _585: https://github.com/ThreeMammals/Ocelot/issues/585\r\n.. _961: https://github.com/ThreeMammals/Ocelot/issues/961\r\n.. _1155: https://github.com/ThreeMammals/Ocelot/pull/1155\r\n.. _2324: https://github.com/ThreeMammals/Ocelot/pull/2324\r\n.. _6.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/6.0.0\r\n.. _15.0.3: https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.3\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _David Lievrouw: https://github.com/DavidLievrouw\r\n.. _500 Internal Server Error: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500\r\n"
  },
  {
    "path": "docs/features/logging.rst",
    "content": ".. _Logging in .NET Core and ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging\r\n.. _Serilog: https://serilog.net\r\n\r\nLogging\r\n=======\r\n\r\n  | MS Learn: `Logging in .NET Core and ASP.NET Core`_\r\n  | Interfaces: `ILoggerFactory <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory>`_ and `ILogger<T> <https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1>`_\r\n\r\nOcelot uses the standard ASP.NET Core logging interfaces ``ILoggerFactory`` and ``ILogger<T>`` at the moment.\r\nThis is encapsulated in `IOcelotLogger <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Logging/IOcelotLogger.cs>`_ and `IOcelotLoggerFactory <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Logging/IOcelotLoggerFactory.cs>`_ with the implementation for the standard `ASP.NET Core logging <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/>`_ stuff at the moment.\r\nThis is because Ocelot adds some extra info to the logs such as :ref:`lg-request-id` if it is configured.\r\n\r\nThere is a global :doc:`../features/errorcodes` :ref:`eh-middleware` that catches any exceptions thrown and logs them as errors.\r\nFinally, if logging is set to the ``Trace`` level, Ocelot will log the start, finish, and any middlewares that throw an exception, which can be quite useful.\r\n\r\n.. _lg-warning:\r\n\r\nWarning\r\n-------\r\n\r\nIf you are logging to the MS `Console <https://learn.microsoft.com/en-us/dotnet/api/system.console>`_, you will experience terrible performance.\r\nThe community has encountered many performance issues with Ocelot, and it is always related to the ``Debug`` logging level when logging to the console/terminal.\r\n\r\n- **Warning**! Make sure you are logging to an appropriate destination/storage in the production environment!\r\n- Use ``Error`` and ``Critical`` levels in the production environment!\r\n- Use the ``Warning`` level in testing & staging environments!\r\n\r\nThese and other recommendations can be found below in the :ref:`lg-best-practices` section.\r\n\r\n.. _lg-best-practices:\r\n\r\nBest Practices\r\n--------------\r\n\r\n  Microsoft Learn complete reference: `Logging in .NET Core and ASP.NET Core`_\r\n\r\nOur recommendations for achieving the best logging with Ocelot are as follows.\r\n\r\n1. Ensure the minimum log level while `Configure logging <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging>`_.\r\n   The minimum log level is set in the application's ``appsettings.json`` file.\r\n   This level is defined in the ``Logging`` section, for example:\r\n\r\n   .. code-block:: json\r\n\r\n    {\r\n      \"Logging\": {\r\n        \"LogLevel\": {\r\n          \"Default\": \"Information\",\r\n          \"Microsoft.AspNetCore\": \"Warning\"\r\n        }\r\n      }\r\n    }\r\n\r\n   Whether you are using `Serilog`_ or the standard Microsoft providers, the logging configuration will be retrieved from this section.\r\n\r\n   .. code-block:: csharp\r\n    :emphasize-lines: 3\r\n\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddJsonFile($\"appsettings.{builder.Environment.EnvironmentName}.json\", false, false) // read logging settings of the environment\r\n        .AddOcelot(builder.Environment);\r\n\r\n   However, there is one thing to be aware of. It is possible to use the ``SetMinimumLevel()`` method to define the minimum logging level.\r\n   Be careful and make sure you set the log level in only one place, like this:\r\n\r\n   .. code-block:: csharp\r\n\r\n    builder.Logging\r\n        .ClearProviders()\r\n        .SetMinimumLevel(LogLevel.Warning);\r\n    // MS Console for Development and/or Testing environments only\r\n    if (!builder.Environment.IsProduction())\r\n    {\r\n        builder.Logging.AddConsole();\r\n    }\r\n\r\n   Please also use the ``ClearProviders()`` method so that only the providers you wish to use are taken into account, such as the console in the example above.\r\n\r\n2. Ensure the proper usage of the minimum logging level for each environment: development, testing, production, etc.\r\n   So, once again, read the important notes in the :ref:`lg-warning` section!\r\n\r\n3. Ocelot's logging has been improved in version `22.0`_:\r\n   it is now possible to use a factory method for message strings that will only be executed if the minimum log level allows it.\r\n\r\n   For example, let's take a message containing information about several variables that should only be generated if the minimum log level is ``Debug``.\r\n   If the minimum log level is ``Warning``, then the string is never generated.\r\n\r\n   Therefore, when the string contains dynamic information (e.g., ``string.Format``), or the string value is generated by a `string interpolation <https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/string-interpolation>`_ expression,\r\n   it is recommended to call the ``LogX`` method using an anonymous delegate via an ``=>`` expression function:\r\n\r\n   .. code-block:: csharp\r\n\r\n    Logger.LogDebug(\r\n        () => $\"Downstream template is {httpContext.Items.DownstreamRoute().DownstreamPathTemplate.Value}\");\r\n\r\n   otherwise a constant string is sufficient\r\n\r\n   .. code-block:: csharp\r\n\r\n    Logger.LogDebug(\"My const string\");\r\n\r\n.. _lg-request-id:\r\n\r\nRequest ID\r\n----------\r\n\r\n  Also known as \"Correlation ID\" or `HttpContext.TraceIdentifier <https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.traceidentifier>`_\r\n\r\nOcelot allows a client to send a *Request ID* through an HTTP header.\r\nIf provided, Ocelot uses the *Request ID* for logging as soon as it becomes available in the middleware pipeline.\r\nAdditionally, Ocelot forwards the *Request ID* via the specified header to the downstream service.\r\n\r\n  * You can still obtain the ASP.NET Core *Request ID* in the logs if you set ``IncludeScopes`` to ``true`` in your logging configuration.\r\n  * The reason for not just using the `bog standard <https://notoneoffbritishisms.com/2015/03/27/bog-standard/>`_ framework logging is that\r\n    we could not work out how to override the ``RequestId`` that gets logged when setting ``IncludeScopes`` to ``true`` in the logging settings.\r\n    Nicely onto the next feature.\r\n\r\nEvery log record has these 2 properties:\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - *Property*\r\n    - *Description*\r\n  * - ``RequestId``\r\n    - This represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001``\r\n  * - ``PreviousRequestId``\r\n    - This represents ID of the previous request\r\n\r\n.. _break: http://break.do\r\n\r\n  As an ``IOcelotLogger`` interface object is injected into the constructors of service classes, the current default Ocelot logger (``OcelotLogger`` class) reads these two properties from the ``IRequestScopedDataRepository`` interface object.\r\n\r\nFind out more about these properties and other details on the *Request ID* logging feature below.\r\n\r\nConfiguration\r\n^^^^^^^^^^^^^\r\n\r\nIn order to use the *Request ID* feature, you have two options: specifying it globally or for the route.\r\n\r\nIn your `ocelot.json`_, set the following configuration in the ``GlobalConfiguration`` section.\r\nThis setting will apply to all requests processed by Ocelot.\r\n\r\n.. code-block:: json\r\n\r\n  \"GlobalConfiguration\": {\r\n    \"RequestIdKey\": \"Oc-RequestId\"\r\n  }\r\n\r\n.. _break: http://break.do\r\n\r\n  We recommend using the ``GlobalConfiguration`` unless it is absolutely necessary to make it route-specific.\r\n\r\nIf you want to override this for a specific route, add the following to `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n\r\n  \"RequestIdKey\": \"Oc-RequestId\"\r\n\r\nOnce Ocelot identifies incoming requests that match a route, it will set the *Request ID* based on the route configuration.\r\n\r\nProblem\r\n^^^^^^^\r\n\r\nThis can lead to a small issue.\r\nIf you set a ``GlobalConfiguration``, it is possible to use one *Request ID* until the route is identified and then another afterward, as the *Request ID* key can change.\r\nThis behavior is intentional and represents the best solution we have devised for now.\r\nIn this case, the ``OcelotLogger`` will display both the current *Request ID* and the previous *Request ID* in the logs.\r\n\r\nBelow is an example of the logging when the ``Debug`` level is set for a normal request:\r\n\r\n  .. code-block:: text\r\n\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[1]\r\n            Request starting HTTP/1.1 GET https://localhost:7778/ocelot2/posts/3 - - -\r\n      dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Ocelot pipeline started\r\n      dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Upstream URL path: /ocelot2/posts/3\r\n      dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Downstream templates: /ocelot/posts/{id}\r\n      info: Ocelot.RateLimiting.Middleware.RateLimitingMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            EnableEndpointEndpointRateLimiting is not enabled for downstream path: /ocelot/posts/{id}\r\n      info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            No authentication needed for path: /ocelot2/posts/3\r\n      info: Ocelot.Authorization.Middleware.AuthorizationMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            No authorization needed for upstream path: /ocelot2/posts/{id}\r\n      dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Downstream URL: http://localhost:5555/ocelot/posts/3\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[1]\r\n            Request starting HTTP/1.1 GET https://localhost:7778/ocelot2/posts/5 - - -\r\n      dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Ocelot pipeline started\r\n      dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Upstream URL path: /ocelot2/posts/5\r\n      dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Downstream templates: /ocelot/posts/{id}\r\n      info: Ocelot.RateLimiting.Middleware.RateLimitingMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            EnableEndpointEndpointRateLimiting is not enabled for downstream path: /ocelot/posts/{id}\r\n      info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            No authentication needed for path: /ocelot2/posts/5\r\n      info: Ocelot.Authorization.Middleware.AuthorizationMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            No authorization needed for upstream path: /ocelot2/posts/{id}\r\n      dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Downstream URL: http://localhost:5555/ocelot/posts/5\r\n      info: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            200 OK status code of request URI: http://localhost:5555/ocelot/posts/3\r\n      dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Setting HTTP response message...\r\n      dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            No pipeline errors: setting and returning completed response...\r\n      dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: -\r\n            Ocelot pipeline finished\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[2]\r\n            Request finished HTTP/1.1 GET https://localhost:7778/ocelot2/posts/3 - 200 84 application/json;+charset=utf-8 404.7256ms\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[16]\r\n            Request reached the end of the middleware pipeline without being handled by application code. Request path: GET https://localhost:7778/ocelot2/posts/3, Response status code: 200\r\n      info: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            200 OK status code of request URI: http://localhost:5555/ocelot/posts/5\r\n      dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Setting HTTP response message...\r\n      dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            No pipeline errors: setting and returning completed response...\r\n      dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]\r\n            RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111\r\n            Ocelot pipeline finished\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[2]\r\n            Request finished HTTP/1.1 GET https://localhost:7778/ocelot2/posts/5 - 200 128 application/json;+charset=utf-8 347.2607ms\r\n      info: Microsoft.AspNetCore.Hosting.Diagnostics[16]\r\n            Request reached the end of the middleware pipeline without being handled by application code. Request path: GET https://localhost:7778/ocelot2/posts/5, Response status code: 200\r\n\r\n.. Note by Maintainer:\r\n..   The PreviousRequestId feature requires review and possible redesign, as it may not be implemented or could be broken.\r\n..   Typically, PreviousRequestId is '-' for all requests.\r\n\r\nTechnical Facts\r\n^^^^^^^^^^^^^^^\r\n\r\n* Every log record has these 2 properties:\r\n\r\n  * ``RequestId`` represents ID of the current request as plain string, for example ``0HNBA3NEIQUNJ:00000001``.\r\n  * ``PreviousRequestId`` represents ID of the previous request.\r\n* As an ``IOcelotLogger`` interface object is injected into the constructors of service classes, the current default Ocelot logger (the ``OcelotLogger`` class) retrieves these two properties from the ``IRequestScopedDataRepository`` service.\r\n\r\n.. _lg-performance:\r\n\r\nPerformance [#f1]_\r\n------------------\r\n\r\nHere is a quick recipe for your production environment to achieve top *performance*.\r\nYou need to ensure the minimum log level is ``Critical`` or ``None``. Nothing more!\r\nHaving top logging *performance* means having fewer log records written by the logging provider. So, the logs should be pretty empty.\r\n\r\nAnyway, during the initial period after a version release to production, we recommend monitoring the system and the current version's app behavior by specifying the minimum log level as ``Error``.\r\nIf the release engineer ensures the stability of the version in production, then the minimum log level can be increased to ``Critical`` or ``None`` to achieve top *performance*.\r\nTechnically, this will disable the logging feature entirely.\r\n\r\nBenchmarks\r\n----------\r\n\r\nWe currently have two types of benchmarks:\r\n\r\n- ``SerilogBenchmarks`` with `Serilog`_ logging to a file. See the ``ConfigureLogging`` method with ``logging.AddSerilog(_logger)``.\r\n- ``MsLoggerBenchmarks`` with MS default logging to the MS Console. See the ``ConfigureLogging`` method with ``logging.AddConsole()``.\r\n\r\nBenchmark results largely depend on the environment and hardware on which they run.\r\nWe are pleased to invite you to run logging benchmarks on your machine by following the instructions below.\r\n\r\n1. Open PowerShell or Command Prompt console\r\n2. Build the Ocelot solution in Release mode: ``dotnet build --configuration Release``\r\n3. Go to the ``test\\Ocelot.Benchmarks\\bin\\Release\\`` folder\r\n4. Choose the .NET version by changing the folder, for example, to ``net9.0``\r\n5. Run benchmarks: ``.\\Ocelot.Benchmarks.exe``\r\n6. Run ``SerilogBenchmarks`` or ``MsLoggerBenchmarks`` by pressing the appropriate number of a benchmark (5 or 6), then press Enter.\r\n7. Wait for 3+ minutes to complete the benchmark and get the final results.\r\n8. Read and analyze your benchmark session results.\r\n\r\n..\r\n      Indicators\r\n      ^^^^^^^^^^\r\n      To be developed...\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] Logging :ref:`performance <lg-performance>` was improved in pull request `1745`_ and released in version `22.0`_.\r\n  These changes were requested as part of issue `1744`_ following the team's discussion in thread `1736`_.\r\n\r\n.. _22.0: https://github.com/ThreeMammals/Ocelot/releases/tag/22.0.0\r\n.. _1745: https://github.com/ThreeMammals/Ocelot/pull/1745\r\n.. _1744: https://github.com/ThreeMammals/Ocelot/issues/1744\r\n.. _1736: https://github.com/ThreeMammals/Ocelot/discussions/1736\r\n\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n"
  },
  {
    "path": "docs/features/metadata.rst",
    "content": "Metadata\n========\n\n  [#f1]_ Feature of: :doc:`../features/configuration`\n\nOcelot provides various features such as routing, authentication, caching, load balancing, and more.\nHowever, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior.\nIn such cases, Ocelot allows users to add *metadata* to the route configuration.\nThis property can store any arbitrary data that users can access in middlewares or delegating handlers.\n\nSchema\n------\n\nAs you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-metadata` section, the route *metadata* schema is quite simple which is JSON dictionary:\n\n.. code-block:: json\n\n  \"Metadata\": {\n    // \"key\": \"value\",\n  }\n\n.. _FileMetadataOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileMetadataOptions.cs\n\nHowever, **global** metadata configuration consists of both the ``Metadata`` and ``MetadataOptions`` sections.\nYou do not need to set all of these things, but this is everything that is available at the moment.\n\n.. code-block:: json\n\n  \"GlobalConfiguration\": {\n    \"Metadata\": {\n      // \"key\": \"value\",\n    },\n    \"MetadataOptions\": {\n      \"CurrentCulture\": \"en-GB\",\n      \"NumberStyle\": \"Any\",\n      \"Separators\": [\",\"],\n      \"StringSplitOption\": \"None\",\n      \"TrimChars\": [\" \"],\n    }\n  }\n\nThe actual global *metadata* schema with all the properties can be found in the C# `FileMetadataOptions`_ class.\nThis configuration type is parsed to a `MetadataOptions <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/MetadataOptions.cs>`_ type object.\n\n.. list-table::\n    :widths: 20 80\n    :header-rows: 1\n\n    * - *Option*\n      - *Description*\n    * - ``CurrentCulture``\n      - | Parsed as the ``System.Globalization.CultureInfo`` object (refer to `CultureInfo <https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-globalization-cultureinfo>`_ class)\n        | Default value is current culture aka ``CultureInfo.CurrentCulture.Name``\n    * - ``NumberStyle``\n      - | Parsed as the ``System.Globalization.NumberStyles`` object (refer to `NumberStyles <https://learn.microsoft.com/en-us/dotnet/api/system.globalization.numberstyles?view=net-9.0>`_ enum)\n        | Default value is ``NumberStyles.Any``\n    * - ``Separators``\n      - Array of ``string``. Default value is ``[\",\"]`` aka comma.\n    * - ``StringSplitOption``\n      - | Parsed as the ``System.StringSplitOptions`` object (refer to `StringSplitOptions <https://learn.microsoft.com/en-us/dotnet/api/system.stringsplitoptions?view=net-9.0>`_ enum)\n        | Default value is ``StringSplitOptions.None``\n    * - ``TrimChars``\n      - Array of ``char``. Default value is ``[\" \"]`` aka whitespace.\n    * - ``Metadata``\n      - | Parsed as the ``Dictionary<string, string>`` object containing all global *metadata* which ``string`` values are parsed to a target type value by the :ref:`md-getmetadata-method`.\n\nConfiguration\n-------------\n\nBy using the *metadata*, users can implement their own logic and extend the functionality of Ocelot e.g.\n\n.. code-block:: json\n\n  {\n    \"Routes\": [\n      {\n        \"UpstreamHttpMethod\": [ \"GET\" ],\n        \"UpstreamPathTemplate\": \"/posts/{postId}\",\n        \"DownstreamPathTemplate\": \"/api/posts/{postId}\",\n        \"DownstreamHostAndPorts\": [\n          { \"Host\": \"localhost\", \"Port\": 80 }\n        ],\n        \"Metadata\": {\n          \"id\": \"FindPost\",\n          \"tags\": \"tag1, tag2, area1, area2, func1\",\n          \"plugin1.enabled\": \"true\",\n          \"plugin1.values\": \"[1, 2, 3, 4, 5]\",\n          \"plugin1.param\": \"value2\",\n          \"plugin1.param2\": \"123\",\n          \"plugin2/param1\": \"overwritten-value\",\n          \"plugin2/data\": \"{\\\"name\\\":\\\"John Doe\\\",\\\"age\\\":30,\\\"city\\\":\\\"New York\\\",\\\"is_student\\\":false,\\\"hobbies\\\":[\\\"reading\\\",\\\"hiking\\\",\\\"cooking\\\"]}\"\n        }\n      }\n    ],\n    \"GlobalConfiguration\": {\n      \"Metadata\": {\n        \"instance_name\": \"machine-1\",\n        \"plugin2/param1\": \"default-value\"\n      },\n      \"MetadataOptions\": {\n      }\n    }\n  }\n\nNow, the route *metadata* can be accessed through the ``DownstreamRoute`` object:\n\n.. code-block:: csharp\n  :emphasize-lines: 20\n\n  using Ocelot.Middleware;\n  using Ocelot.Metadata;\n  using Ocelot.Logging;\n\n  public class MyMiddleware : OcelotMiddleware\n  {\n      private readonly RequestDelegate _next;\n      private readonly IMyService _myService;\n\n      public MyMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IMyService myService)\n          : base(loggerFactory.CreateLogger<MyMiddleware>())\n      {\n          _next = next;\n          _myService = myService;\n      }\n\n      public Task Invoke(HttpContext context)\n      {\n          Logger.LogDebug(\"My middleware started\");\n          var route = context.Items.DownstreamRoute();\n          var id = route.GetMetadata<string>(\"id\");\n          var tags = route.GetMetadata<string[]>(\"tags\");\n\n          // Plugin 1 data\n          var p1Enabled = route.GetMetadata<bool>(\"plugin1.enabled\");\n          var p1Values = route.GetMetadata<string[]>(\"plugin1.values\");\n          var p1Param = route.GetMetadata<string>(\"plugin1.param\", \"system-default-value\");\n          var p1Param2 = route.GetMetadata<int>(\"plugin1.param2\");\n\n          // Plugin 2 data\n          var p2Param1 = route.GetMetadata<string>(\"plugin2/param1\", \"default-value\");\n          var json = route.GetMetadata<string>(\"plugin2/data\");\n          var plugin2 = System.Text.Json.JsonSerializer.Deserialize<Plugin2Data>(json);\n\n          // Reading global metadata\n          var globalInstanceName = route.GetMetadata<string>(\"instance_name\");\n          var globalPlugin2Param1 = route.GetMetadata<string>(\"plugin2/param1\");\n\n          // Working with plugin's metadata\n          // ...\n          return _next.Invoke(context);\n      }\n      public class Plugin2Data\n      {\n          public string name { get; set; }\n          public int age { get; set; }\n          public string city { get; set; }\n          public bool is_student { get; set; }\n          public string[] hobbies { get; set; }\n      }\n  }\n\n.. _md-getmetadata-method:\n\n``GetMetadata<T>`` Method\n-------------------------\n\nOcelot provides one ``DowstreamRoute`` extension method to help you retrieve your *metadata* values effortlessly.\nWith the exception of the types ``string``, ``bool``, ``bool?``, ``string[]`` and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T.\nIf the value is null, then, if not explicitely specified, the default for the chosen target type is returned.\n\n.. list-table::\n    :widths: 20 80\n    :header-rows: 1\n\n    * - *Method*\n      - *Description*\n    * - ``GetMetadata<string>``\n      - The *metadata* value is returned as string without further parsing\n    * - ``GetMetadata<string[]>``\n      - | The *metadata* value is splitted by a given separator (default ``,``) and returned as a string array.\n        | **Note**: Several parameters can be set in the global configuration, such as ``Separators`` (default = ``[\",\"]``), ``StringSplitOptions`` (default ``None``) and ``TrimChars``, the characters that should be trimmed (default = ``[' ']``).\n    * - ``GetMetadata<TInt>`` \n      - | The *metadata* value is parsed to a number. The ``TInt`` is any known numeric type, such as ``byte``, ``sbyte``, ``short``, ``ushort``, ``int``, ``uint``, ``long``, ``ulong``, ``float``, ``double``, ``decimal``.\n        | **Note**: Some parameters can be set in the global configuration, such as ``NumberStyle`` (default ``Any``) and ``CurrentCulture`` (default ``CultureInfo.CurrentCulture``)\n    * - ``GetMetadata<T>``\n      - | The *metadata* value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type.\n        | **Note**: A ``JsonSerializerOptions`` object can be passed as method parameter, ``Web`` is used as default.\n    * - ``GetMetadata<bool>``\n      - | Check if the *metadata* value is a truthy value, otherwise return ``false``.\n        | **Note**: The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``\n    * - ``GetMetadata<bool?>``\n      - | Check if the *metadata* value is a truthy value (return ``true``), or falsy value (return ``false``), otherwise return ``null``.\n        | **Note**: The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0``\n\nSample\n------\n\nThe *Metadata* feature is a relatively new :doc:`../features/configuration` feature (anchored in the \":ref:`config-route-metadata`\" section).\n\nTo introduce a standardized approach to middleware development, we have prepared a comprehensive sample project:\n\n  | **Project**: `samples <https://github.com/ThreeMammals/Ocelot/tree/main/samples>`_ / `Metadata <https://github.com/ThreeMammals/Ocelot/tree/main/samples/Metadata>`_\n  | **Solution**: `Ocelot.Samples.sln <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Ocelot.Samples.sln>`_\n\nThe solution for the ``Ocelot.Samples.Metadata.csproj`` project includes the following capabilities:\n\n- It has two custom Ocelot middlewares attached: ``PreErrorResponderMiddleware`` and ``ResponderMiddleware``.\n  The ``PreErrorResponderMiddleware`` reads the route *metadata* based on the route ID and parses it.\n  This is an example of how to parse or read the *metadata* of a specific route.\n- The custom ``ResponderMiddleware`` simply calls the base Ocelot middleware (default implementation).\n  Ocelot's ``ResponderMiddleware`` is responsible for writing the final body data into the ``HttpResponse`` of the current ``HttpContext``.\n- The main `Program`_ replaces Ocelot's default ``IHttpResponder`` service with a custom ``MetadataResponder`` service.\n  It attaches both ``PreErrorResponderMiddleware`` and ``ResponderMiddleware`` using the ``OcelotPipelineConfiguration`` argument in the ``UseOcelot`` method.\n- The ``MetadataResponder`` service processes all JSON data when the ``Content-Type`` header has the value ``application/json``.\n  This custom responder service writes the original data into the ``Response`` section and writes the route *metadata* back to the ``Metadata`` section using the following JSON schema:\n\n    .. code-block:: json\n\n      {\n        \"Response\": {\n          // Original data of the downstream response\n        },\n        \"Metadata\": {\n          // current route metadata\n        }\n      }\n\n- The ``MetadataResponder`` service always generates the custom ``OC-Route-Metadata`` header, containing the route *metadata* as a plain JSON string for all routes, regardless of the media type of the content.\n  This allows you to parse it on the client side for specific purposes.\n- The ``MetadataResponder`` service attempts to decompress the content body if it is compressed using one of the following algorithms from downstream endpoints: Brotli (``br``), GZip (``gzip``), or Zstandard (``zstd``).\n  However, data compressed with the ``deflate`` algorithm is ignored and transferred to the client as-is because decompressing a third-party algorithm with a custom implementation is not feasible.\n  Finally, the responder service returns uncompressed data and indicates this in the ``Content-Encoding`` header, where the value is always set to ``identity``.\n- Processing JSON data can be disabled for specific routes using the ``disableMetadataJson`` option in the *metadata*.\n  In this case, all JSON data is returned to the client as-is, preserving the original body streams (see the ``/ocelot/docs/`` route).\n\n**Conclusion**: The purpose of this sample is to detect JSON data, process it, and embed a custom ``Metadata`` section while returning the original JSON data in the ``Response`` section.\nThis sample and its ``MetadataResponder`` service significantly increase response time due to on-the-fly JSON data processing, leading to degraded overall performance.\nPlease consider this as an example of processing *metadata*. For production environments, such processing should be disabled.\nInstead, returning *metadata* in a custom header is likely the best solution if your client needs to know the currently executed route on Ocelot's side.\n\n\"\"\"\"\n\n.. [#f1] The *Metadata* feature was requested in issues `738`_ and `1990`_, and it was released as part of version `23.3`_.\n\n.. _738: https://github.com/ThreeMammals/Ocelot/issues/738\n.. _1990: https://github.com/ThreeMammals/Ocelot/issues/1990\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Metadata/Program.cs\n"
  },
  {
    "path": "docs/features/methodtransformation.rst",
    "content": "Method Transformation [#f1]_\r\n============================\r\n\r\nOcelot allows users to modify the HTTP request method used when making requests to a downstream service.\r\nThis is achieved by setting the following route configuration:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/{everything}\",\r\n    \"DownstreamPathTemplate\": \"/{everything}\",\r\n    // other props and opts...\r\n    \"UpstreamHttpMethod\": [ \"Get\" ], // we transform HTTP verb...\r\n    \"DownstreamHttpMethod\": \"Post\" // ...from GET to POST\r\n  }\r\n\r\nThe key property here is ``DownstreamHttpMethod``, which is set to ``POST``, and the route will only match ``GET``, as specified by ``UpstreamHttpMethod``.\r\n\r\nThis feature is useful when interacting with downstream APIs that only support ``POST`` while presenting a RESTful interface.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The *\"Method Transformation\"* feature was released in version `14.0.8`_.\r\n.. _14.0.8: https://github.com/ThreeMammals/Ocelot/releases/tag/14.0.8\r\n"
  },
  {
    "path": "docs/features/middlewareinjection.rst",
    "content": ".. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Metadata/Program.cs\r\n\r\nMiddleware Injection\r\n====================\r\n\r\n\r\nWhen setting up Ocelot in your `Program`_, you can provide additional middleware and override it with your custom middlewares. This is done as follows:\r\n\r\n.. code-block:: csharp\r\n\r\n    // Set it up: configuration, services, etc.\r\n    // Middleware setup is only possible during the final stage of app configuration and execution\r\n    var app = builder.Build();\r\n    var pipeline = new OcelotPipelineConfiguration\r\n    {\r\n        PreErrorResponderMiddleware = async (context, next) =>\r\n        {\r\n            await next.Invoke();\r\n        }\r\n    };\r\n    await app.UseOcelot(pipeline);\r\n    await app.RunAsync();\r\n\r\nIn the example above, the provided function will run before the first piece of Ocelot middleware.\r\nThis allows users to supply any behavior they want before and after the Ocelot pipeline has run.\r\n\r\n.. warning::\r\n    Be cautious, as this means you can break everything — use at your own risk or pleasure!\r\n    If you notice any exceptions or strange behavior in your middleware pipeline and are using any of the following, remove your custom middlewares and try again.\r\n\r\n.. _mi-ocelotpipelineconfiguration-class:\r\n\r\n``OcelotPipelineConfiguration`` Class\r\n-------------------------------------\r\n\r\n.. _OcelotPipelineConfiguration: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs\r\n\r\n  Class: `OcelotPipelineConfiguration`_\r\n\r\nThe user can set middleware-functions aka custom user's middleware against the following:\r\n\r\n.. list-table::\r\n    :widths: 50 50\r\n    :header-rows: 1\r\n\r\n    * - *Middleware*\r\n      - *Description*\r\n    * - | ``PreErrorResponderMiddleware``\r\n        | Prev: ``ExceptionHandlerMiddleware``\r\n        | Next: ``ResponderMiddleware``\r\n      - This is called after the global error-handling middleware, so any code before calling ``next.Invoke`` is the next action executed in the Ocelot pipeline.\r\n        Any code after ``next.Invoke`` is the final action executed in the Ocelot pipeline before reaching the global error handler.\r\n    * - | ``ResponderMiddleware``\r\n        | Prev: ``PreErrorResponderMiddleware``\r\n        | Next: ``DownstreamRouteFinderMiddleware``\r\n      - This allows the user to completely override Ocelot's `ResponderMiddleware <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs>`_. :sup:`1`\r\n    * - | ``PreAuthenticationMiddleware``\r\n        | Prev: ``RequestIdMiddleware``\r\n        | Next: ``AuthenticationMiddleware``\r\n      - This allows the user to run any extra authentication before the Ocelot authentication kicks in.\r\n    * - | ``AuthenticationMiddleware``\r\n        | Prev: ``PreAuthenticationMiddleware``\r\n        | Next: ``ClaimsToClaimsMiddleware``\r\n      - This allows the user to completely override Ocelot's `AuthenticationMiddleware <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs>`_. :sup:`1`\r\n    * - | ``PreAuthorizationMiddleware``\r\n        | Prev: ``ClaimsToClaimsMiddleware``\r\n        | Next: ``AuthorizationMiddleware``\r\n      - This allows the user to run any extra authorization before the Ocelot authorization kicks in.\r\n    * - | ``AuthorizationMiddleware``\r\n        | Prev: ``PreAuthorizationMiddleware``\r\n        | Next: ``ClaimsToHeadersMiddleware``\r\n      - This allows the user to completely override Ocelot's `AuthorizationMiddleware <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs>`_. :sup:`1`\r\n    * - | ``ClaimsToHeadersMiddleware``\r\n        | Prev: ``AuthorizationMiddleware``\r\n        | Next: ``PreQueryStringBuilderMiddleware``\r\n      - This allows the user to completely override Ocelot's `ClaimsToHeadersMiddleware <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs>`_. :sup:`1`\r\n    * - | ``PreQueryStringBuilderMiddleware``\r\n        | Prev: ``ClaimsToHeadersMiddleware``\r\n        | Next: ``ClaimsToQueryStringMiddleware``\r\n      - This allows the user to implement own query string manipulation logic.\r\n\r\nObviously, you can add the mentioned Ocelot middleware overrides as normal before the call to ``app.UseOcelot``.\r\nThey cannot be added afterward because Ocelot does not invoke subsequent middleware overrides based on the specified middleware configuration.\r\nAs a result, the next-called middleware **will not** affect the Ocelot configuration.\r\n\r\n.. warning::\r\n  :sup:`1` Use the mentioned middleware overrides with caution! Overridden middleware removes the default implementation.\r\n  If you encounter any exceptions or strange behavior in your middleware pipeline, remove the overridden middleware and try again.\r\n\r\n.. _mi-ocelot-pipeline-builder:\r\n\r\nOcelot Pipeline Builder\r\n-----------------------\r\n\r\n  | Class: ``Ocelot.Middleware.OcelotPipelineExtensions``\r\n  | Method: ``BuildOcelotPipeline(IApplicationBuilder, OcelotPipelineConfiguration)``\r\n\r\nThe Ocelot pipeline is part of the entire `ASP.NET Core Middleware <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/>`_ conveyor, also known as the app pipeline.\r\nThe `BuildOcelotPipeline <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+BuildOcelotPipeline+path%3A%2F%5Esrc%5C%2FOcelot%5C%2FMiddleware%5C%2F%2F&type=code>`_ method encapsulates the Ocelot pipeline.\r\nThe last middleware in the ``BuildOcelotPipeline`` method is ``HttpRequesterMiddleware``, which calls the next middleware if it is added to the pipeline.\r\n\r\nThe internal `HttpRequesterMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+HttpRequesterMiddleware+path%3A%2F%5Esrc%5C%2FOcelot%5C%2F%2F&type=code>`_ is part of the pipeline,\r\nbut it is private and cannot be overridden since this middleware is not included in the list of `user-accessible public middlewares <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs>`_ that can be overridden.\r\nTherefore, it is the `final middleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20app.UseMiddleware%3CHttpRequesterMiddleware%3E()&type=code>`_ in both the Ocelot and ASP.NET pipelines, and it handles non-user operations.\r\nThe last user (public) middleware that can be overridden is `PreQueryStringBuilderMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+PreQueryStringBuilderMiddleware+language%3AC%23&type=code&l=C%23>`_, which is read from the pipeline configuration object.\r\nFor more details, see the previous :ref:`mi-ocelotpipelineconfiguration-class` section.\r\n\r\nTo understand the actual order of middleware execution, here is a quick list of them, with an asterisk (*) marking the ones that can be overridden:\r\n\r\n1. ``ConfigurationMiddleware``\r\n2. ``ExceptionHandlerMiddleware``\r\n3. ``PreErrorResponderMiddleware``\\*\r\n4. ``ResponderMiddleware``\\*\r\n5. ``DownstreamRouteFinderMiddleware``\r\n6. ``MultiplexingMiddleware``\r\n7. ``SecurityMiddleware``\r\n8. ``HttpHeadersTransformationMiddleware``\r\n9. ``DownstreamRequestInitialiserMiddleware``\r\n10. ``RateLimitingMiddleware``\r\n11. ``RequestIdMiddleware``\r\n12. ``PreAuthenticationMiddleware``\\*\r\n13. ``AuthenticationMiddleware``\\*\r\n14. ``ClaimsToClaimsMiddleware``\r\n15. ``PreAuthorizationMiddleware``\\*\r\n16. ``AuthorizationMiddleware``\\*\r\n17. ``ClaimsToHeadersMiddleware``\\*\r\n18. ``PreQueryStringBuilderMiddleware``\\*\r\n19. ``ClaimsToQueryStringMiddleware``\r\n20. ``ClaimsToDownstreamPathMiddleware``\r\n21. ``LoadBalancingMiddleware``\r\n22. ``DownstreamUrlCreatorMiddleware``\r\n23. ``OutputCacheMiddleware``\r\n24. ``HttpRequesterMiddleware``\r\n\r\nConsidering that ``PreQueryStringBuilderMiddleware`` and ``HttpRequesterMiddleware`` are the final user and system middleware, there are no other middleware components in the pipeline.\r\nHowever, you can still extend the ASP.NET pipeline, as demonstrated in the following code:\r\n\r\n.. code-block:: csharp\r\n\r\n    await app.UseOcelot();\r\n    app.UseMiddleware<MyCustomMiddleware>();\r\n\r\nHowever, we do not recommend adding custom middleware before or after calling ``UseOcelot()`` because it affects the stability of the entire pipeline and has not been tested.\r\nThis type of custom pipeline building falls outside the Ocelot pipeline model, and the quality of the solution is your responsibility.\r\n\r\nFinally, do not confuse the distinction between system (private, non-overridden) and user (public, overridden) middleware.\r\nPrivate middleware is hidden and cannot be overridden, but the entire ASP.NET pipeline can still be extended.\r\nThe public middleware of the :ref:`mi-ocelotpipelineconfiguration-class` is fully customizable and can be overridden.\r\n\r\nRoadmap\r\n-------\r\n\r\nThe community has shown interest in adding more overridden middleware.\r\nOne such request is pull request `1497 <https://github.com/ThreeMammals/Ocelot/pull/1497>`_, which may possibly be included in an upcoming release.\r\n\r\nIn any case, if the current overridden middleware does not provide enough pipeline flexibility, you can open a new topic in the `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ of the repository. |octocat|\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n"
  },
  {
    "path": "docs/features/qualityofservice.rst",
    "content": ".. role:: htm(raw)\n  :format: html\n.. role:: pdf(raw)\n  :format: latex pdflatex\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\n.. _Polly: https://www.pollydocs.org\n.. _documentation: https://www.pollydocs.org\n.. _Resilience strategies: https://www.pollydocs.org/strategies/index.html\n.. |QoS_label| image:: https://img.shields.io/badge/-QoS-D3ADAF.svg\n  :target: https://github.com/ThreeMammals/Ocelot/labels/QoS\n  :alt: label QoS\n  :class: img-valign-textbottom\n\nQuality of Service\n==================\n\n  Repository Label: |QoS_label|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/labels/QoS}{QoS}`\n\nOcelot currently supports a single *Quality of Service* (QoS) capability.\nIt allows you to configure, on a per-route basis, the application of a circuit breaker when making requests to downstream services.\nThis feature leverages a well-regarded .NET library known as `Polly`_.\nFor more details, visit the `Polly`_ library's official `repository <https://github.com/App-vNext/Polly>`_.\n\n.. note::\n  \n  `Polly`_ v7 syntax is no longer supported as of version `23.2`_, when the Ocelot team upgraded Polly `from v7 to v8 <https://www.pollydocs.org/migration-v8.html>`_.\n\nInstallation\n------------\n\nTo utilize the *Quality of Service* via `Polly`_ library, begin by importing the appropriate `Ocelot.Provider.Polly <https://www.nuget.org/packages/Ocelot.Provider.Polly>`_ extension package:\n\n.. code-block:: powershell\n\n    Install-Package Ocelot.Provider.Polly\n\nNext, in your `Program`_, incorporate `Polly`_ services by invoking the ``AddPolly()`` extension on the ``OcelotBuilder``, as shown below [#f1]_:\n\n.. code-block:: csharp\n  :emphasize-lines: 5\n\n  using Ocelot.Provider.Polly;\n\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddPolly();\n\n.. _qos-schema:\n\n``QoSOptions`` Schema\n---------------------\n.. _MinimumThroughput: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput\n.. _BreakDuration: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration\n.. _FailureRatio: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio\n.. _SamplingDuration: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration\n.. _Timeout: https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout\n.. _FileQoSOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileQoSOptions.cs\n\n  Class: `FileQoSOptions`_\n\nHere is the complete *Quality of Service* configuration, also known as the \"QoS options schema\".\nDepending on your needs and choosen strategies definition of all properties are not required.\nIf you skip a property then a default value will be substituted as per Ocelot/Polly specification.\n\n.. code-block:: json\n\n  \"QoSOptions\": {\n    // Circuit Breaker strategy\n    \"BreakDuration\": 0, // integer\n    \"MinimumThroughput\": 0, // integer\n    \"FailureRatio\": 0.0, // floating number\n    \"SamplingDuration\": 0, // integer\n    // Timeout strategy\n    \"Timeout\": 0, // integer\n    // Deprecated options\n    \"DurationOfBreak\": 0, // deprecated! -> use BreakDuration\n    \"ExceptionsAllowedBeforeBreaking\": 0, // deprecated! -> use MinimumThroughput\n    \"TimeoutValue\": 0, // deprecated! -> use Timeout\n  }\n\n.. list-table::\n    :widths: 30 70\n    :header-rows: 1\n\n    * - *Ocelot Option and Polly equivalent*\n      - *Description*\n    * - ``BreakDuration`` (formerly ``DurationOfBreak``) as `BreakDuration`_\n      - This is duration of break the circuit will stay open before resetting. The unit is milliseconds.\n    * - ``MinimumThroughput`` (formerly ``ExceptionsAllowedBeforeBreaking``) as `MinimumThroughput`_, a primary option\n      - This number of actions or more must pass through the circuit within the time slice for the statistics to be considered significant and for the circuit breaker to engage\n    * - ``FailureRatio`` is `FailureRatio`_\n      - This is the failure-to-success ratio at which the circuit will break\n    * - ``SamplingDuration`` is `SamplingDuration`_\n      - This is the duration of the sampling over which failure ratios are assessed. The unit is milliseconds.\n    * - ``Timeout`` (formerly ``TimeoutValue``) as `Timeout`_, a primary option\n      - This is the default timeout. The unit is milliseconds.\n\n.. warning::\n  The following options are deprecated in version `24.1`_: ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue``!\n  Use the appropriate new options as shown in the table above.\n  These deprecated options will be removed in version `25.0`_.\n  For backward compatibility in version `24.1`_, a deprecated option takes precedence over its replacement.\n\n.. _break1: http://break.do\n\n  **Note** [#f2]_: Ocelot checks that the values of options are valid during execution.\n  If not, it logs errors or warnings (refer to the :ref:`qos-notes-value-constraints` section in :ref:`qos-notes`).\n  For a complete explanation about strategies and mechanisms, consult Polly's `Resilience strategies`_ documentation.\n\n.. _qos-global-configuration:\n\nGlobal Configuration [#f3]_\n---------------------------\n\nAccording to the :ref:`config-global-configuration-schema`, global *Quality of Service* options for static routes were introduced in version `24.1`_.\nThese global options can also be overridden in the ``Routes`` configuration section, a capability that has been supported for a long time.\n\n.. code-block:: json\n  :emphasize-lines: 5-7, 12, 18-21\n\n  {\n    \"Routes\": [\n      {\n        \"Key\": \"R0\", // optional\n        \"QoSOptions\": {\n          \"Timeout\": 15000 // 15s\n        },\n        // ...\n      },\n      {\n        \"Key\": \"R1\", // this route is part of a group\n        \"QoSOptions\": {}, // optional due to grouping\n        // ...\n      }\n    ],\n    \"GlobalConfiguration\": {\n      \"BaseUrl\": \"https://ocelot.net\",\n      \"QoSOptions\": {\n        \"RouteKeys\": [\"R1\",], // if undefined or empty array, opts will apply to all routes\n        \"BreakDuration\": 1000, // 1s\n        \"MinimumThroughput\": 3\n      },\n      // ...\n    }\n  }\n\nDynamic routes were not supported in versions prior to `24.1`_.\nHowever, global *Quality of Service* options have been available in :ref:`Dynamic Routing <routing-dynamic>` mode for a long time.\nStarting with version `24.1`_, global *QoS* options can also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`.\n\n.. code-block:: json\n  :emphasize-lines: 6-8, 17-22\n\n  {\n    \"DynamicRoutes\": [\n      {\n        \"Key\": \"\", // optional\n        \"ServiceName\": \"my-service\",\n        \"QoSOptions\": {\n          \"Timeout\": 15000 // 15s\n        },\n      }\n    ],\n    \"GlobalConfiguration\": {\n      \"BaseUrl\": \"https://ocelot.net\",\n      \"DownstreamScheme\": \"http\",\n      \"ServiceDiscoveryProvider\": {\n        // required section for dynamic routing\n      },\n      \"QoSOptions\": {\n        \"RouteKeys\": [], // or null, no grouping, thus opts apply to all dynamic routes\n        \"BreakDuration\": 1000, // 1s\n        \"MinimumThroughput\": 3,\n        \"FailureRatio\": 0.1, // 10%\n        \"SamplingDuration\": 30000 // 30s\n      }\n    }\n  }\n\nIn this dynamic routing configuration, the :ref:`qos-timeout-strategy` is applied to the ``my-service`` service in addition to the :ref:`qos-circuit-breaker-strategy`, resulting in `Polly`_ timing out after 15 seconds.\nHowever, for all implicit dynamic routes, the :ref:`qos-timeout-strategy` is not globally configured, in favor of the standard :ref:`config-timeout` option managed by the Ocelot Core requester middleware.\nLastly, the :ref:`qos-circuit-breaker-strategy` has been globally configured for all routes due to the absence of route grouping, with the following options:\nallow 3 errors before breaking the circuit for 1 second, and allow up to 10% errors during the default 30-second sampling period.\n\n.. note::\n\n  1. Please note that\n  route-level options take precedence over global options.\n\n  2. If the ``RouteKeys`` option is not defined or the array is empty in the global ``QoSOptions``, the global options will apply to all routes.\n  If the array contains route keys, it defines a single group of routes to which the global options apply.\n  Routes excluded from this group must specify their own route-level ``QoSOptions``.\n\n  3. Since Ocelot's Polly provider utilizes the `Resilience pipeline registry`_, each route has a dedicated pipeline cached in Polly's registry using the route's load-balancing key.\n  For a static route, the load-balancing key uniquely identifies the route by its upstream options, whereas for dynamic routes the load-balancing key is typically the service name from the discovery provider.\n  Thus, Polly's registry maintains dedicated pipelines for each discovered service, and those pipelines behave independently.\n  Finally, it is important to understand that global *QoS* options do not create a single shared resilience pipeline in the registry.\n\n  4. Dynamic routes were not supported in versions prior to `24.1`_.\n  Beginning with version `24.1`_, global *QoS* options for :ref:`Dynamic Routing <routing-dynamic>` may be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`.\n  Additionally, global configuration for static routes (also known as ``Routes``) has been supported since version `24.1`_.\n\n.. _Resilience pipeline registry: https://www.pollydocs.org/pipelines/resilience-pipeline-registry.html\n.. _qos-circuit-breaker-strategy:\n\nCircuit Breaker strategy\n------------------------\n.. _Circuit breaker resilience strategy: https://www.pollydocs.org/strategies/circuit-breaker.html\n\n  | Documentation: `Circuit breaker resilience strategy`_\n  | Primary option: ``MinimumThroughput``, formerly ``ExceptionsAllowedBeforeBreaking``\n\nThe options ``MinimumThroughput`` and ``BreakDuration`` can be configured independently from ``Timeout``:\n\n.. code-block:: json\n\n  \"QoSOptions\": {\n    \"MinimumThroughput\": 3,\n    \"BreakDuration\": 1000 // ms\n  }\n\nAlternatively, you can omit ``BreakDuration``, which will default to the implicit 5-second setting as specified in Polly's `BreakDuration`_ documentation:\n\n.. code-block:: json\n\n  \"QoSOptions\": {\n    \"MinimumThroughput\": 3\n  }\n\nThis setup activates only the `Circuit breaker resilience strategy`_.\n\nAdditionally, there is a failure handling strategy based on ``FailureRatio``, which serves as a counterpart to, or supplement for, the number of failures, also known as ``MinimumThroughput``.\n\n.. code-block:: json\n\n  \"QoSOptions\": {\n    \"MinimumThroughput\": 10,\n    \"FailureRatio\": 0.5, // 50%\n    \"SamplingDuration\": 10000, // ms, 10 seconds\n  }\n\nThus, a failure ratio of ``0.5`` indicates that the circuit will break if 50% or more of actions result in handled failures, after reaching the minimum threshold of 10 failures, also known as the ``MinimumThroughput`` option.\nAdditionally, the 10-second sampling duration defines the time window over which the 50% failure ratio is evaluated.\n\n  **Note**: The ``MinimumThroughput`` option (also known as Polly's `MinimumThroughput`_) is the primary option that enables the *Circuit Breaker strategy*.\n  Its value must be valid (set to 2 or greater, refer to the :ref:`qos-notes-value-constraints` section in :ref:`qos-notes`) and may be supplemented with additional Circuit Breaker options.\n\n.. _qos-timeout-strategy:\n\nTimeout strategy\n----------------\n.. _Timeout resilience strategy: https://www.pollydocs.org/strategies/timeout.html\n\n  | Documentation: `Timeout resilience strategy`_\n  | Primary option: ``Timeout``, formerly ``TimeoutValue``\n\nThe ``Timeout`` can be configured independently from the options of the :ref:`qos-circuit-breaker-strategy`:\n\n.. code-block:: json\n\n  \"QoSOptions\": {\n    \"Timeout\": 5000 // ms\n  }\n\nThis setup activates only the `Timeout resilience strategy`_.\n\nTo configure a global QoS timeout using the *Timeout strategy* for all routes (both static and dynamic) set the ``Timeout`` option as defined in the :ref:`config-global-configuration-schema`:\n\n.. code-block:: json\n\n  \"GlobalConfiguration\": {\n    // other global props\n    \"QoSOptions\": {\n      \"Timeout\": 10000 // ms, 10 seconds\n    }\n  }\n\nPlease note that the route-level timeout takes precedence over the global timeout.\nFor example, a route timeout may be shorter, while the global timeout can be longer and apply to all routes.\n\n  **Note**: There are :ref:`qos-notes-value-constraints` for ``Timeout``: it must be a positive number starting from *1 millisecond* to enable the *Timeout strategy*.\n  If ``Timeout`` is undefined, zero or a negative number, the *Timeout strategy* will not be added to the resilience pipeline.\n  Also, keep in mind Polly's `Timeout`_ constraint, thus Ocelot validates the ``Timeout``.\n  If the value violates Polly's requirements, it will be rolled back to the default of *30 seconds*.\n\n.. _qos-notes:\n\nNotes\n-----\n.. _DefTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22const+int+DefTimeout%22&type=code\n.. _DefaultTimeoutSeconds: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22static+int+DefaultTimeoutSeconds%22&type=code\n.. _DefaultTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+DefaultTimeout+path%3A%2F%5Esrc%5C%2FOcelot.Provider.Polly%5C%2F%2F&type=code\n\n\n.. _qos-notes-absolute-timeout:\n\nAbsolute timeout [#f4]_\n^^^^^^^^^^^^^^^^^^^^^^^\n\nIf a *QoS* section is not included, *QoS* will not be applied, and Ocelot will enforce an absolute timeout of 90 seconds (defined by the ``DownstreamRoute`` `DefTimeout`_ constant) for all downstream requests.\nThis absolute timeout is configurable via the ``DownstreamRoute`` `DefaultTimeoutSeconds`_ static C# property.\nFor more information, refer to the :ref:`config-default-timeout` section of the :doc:`../features/configuration` chapter.\n\n.. _qos-notes-value-constraints:\n\nValue constraints\n^^^^^^^^^^^^^^^^^\n\nStarting with `Polly`_ v8, the `Resilience strategies`_ documentation outlines the following constraints on values:\n\n* The ``BreakDuration`` value must exceed **500** milliseconds and be less than **24** hours (1 day = ``86 400 000`` milliseconds).\n  If unspecified or invalid, it defaults to **5000** milliseconds (5 seconds); refer to the `BreakDuration`_ documentation.\n* The ``MinimumThroughput`` value must be **2** or greater.\n  If unspecified or invalid, it defaults to **100** failures; refer to the `MinimumThroughput`_ documentation.\n* The ``FailureRatio`` must be greater than **0.0** and no more than **1.0**.\n  If unspecified or invalid, it defaults to **0.1** (10%); refer to the `FailureRatio`_ documentation.\n* The ``SamplingDuration`` value must exceed **500** milliseconds and be less than **24** hours (1 day = ``86 400 000`` milliseconds).\n  If unspecified or invalid, it defaults to **30000** milliseconds (30 seconds); refer to the `SamplingDuration`_ documentation.\n* The ``Timeout`` must be greater than **10** milliseconds and less than **24** hours (1 day = ``86 400 000`` milliseconds).\n  If unspecified or invalid, it defaults to **30000** milliseconds (30 seconds); refer to the `Timeout`_ documentation.\n  And please note, when both route-level and global *QoS* timeouts have positive values but are invalid, a default value will be automatically substituted from the ``TimeoutStrategy`` class `DefaultTimeout`_ static C# property, which can also be configured in your `Program`_.\n\nOcelot logs warnings containing failed validation messages for all options, but it does not block Ocelot startup, even when *QoS* options are invalid.\nInspect your logs for these messages and adjust your configuration if necessary.\n\n.. _qos-notes-qos-and-route-global-timeouts:\n\nQoS and route (global) timeouts\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe ``Timeout`` option in *QoS* always takes precedence over the route :ref:`config-timeout` property, so :ref:`config-timeout` will be ignored in favor of QoS ``Timeout``.\nIn Ocelot Core, ``Timeout`` and configuration :ref:`config-timeout` are not intended to be used together.\nMoreover, there is an Ocelot Core design constraint: if the route or global ``Timeout`` duration is shorter than the *QoS* ``Timeout``, you may encounter warning messages in the logs that begin with the following sentence:\n\n.. code-block:: text\n\n  Route '/xxx' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: ...\n\nThis warning means that the route or global timeout will occur before the *QoS* :ref:`qos-timeout-strategy` has a chance to handle its own timeout event, which is configured with a longer duration.\nTechnically, this situation results in the functional disabling of the Polly's `Timeout resilience strategy`_.\nOcelot handles this misconfiguration by logging a warning and automatically applying a longer timeout to the ``TimeoutDelegatingHandler`` in order to effectively unblock the *QoS* :ref:`qos-timeout-strategy`.\nTo avoid this warning, ensure that your *QoS* timeouts are shorter than the route or global timeouts, or remove the :ref:`config-timeout` property from routes where *QoS* is enabled with the ``Timeout`` option.\n\n.. _qos-notes-global-and-default-qos-timeouts:\n\nGlobal and default QoS timeouts\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIf a route-level *QoS* timeout is undefined, the global ``Timeout`` takes precedence over the default timeout (30 seconds, see the `Timeout`_ docs).\nThis means the global *QoS* timeout can override Polly's default of `30 seconds <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22const+int+DefTimeout%22+path%3A%2F%5Esrc%5C%2FOcelot%5C.Provider%5C.Polly%5C%2F%2F&type=code>`_ via the :ref:`config-global-configuration-schema`.\n\n.. _qos-extensibility:\n\nExtensibility [#f5]_\n--------------------\n\nTo use your ``ResiliencePipeline<T>`` provider, you can apply the following syntax:\n\n.. code-block:: csharp\n  :emphasize-lines: 3\n\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddPolly<MyProvider>();\n  // MyProvider should implement IPollyQoSResiliencePipelineProvider<HttpResponseMessage> \n  // Note: you can use standard provider PollyQoSResiliencePipelineProvider\n\nAdditionally, if you want to utilize your own ``DelegatingHandler``, the following syntax can be applied:\n\n.. code-block:: csharp\n  :emphasize-lines: 3\n\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddPolly<MyProvider>(MyQosDelegatingHandlerDelegate);\n  // MyQosDelegatingHandlerDelegate is a delegate use to get a DelegatingHandler. Refer to Ocelot's PollyResiliencePipelineDelegatingHandler\n\nFinally, to define your own set of exceptions for mapping, you can apply the following syntax:\n\n.. code-block:: csharp\n  :emphasize-lines: 11\n\n  static Error CreateError(Exception e) => new RequestTimedOutError(e);\n  Dictionary<Type, Func<Exception, Error>> MyErrorMapping = new()\n  {\n      {typeof(TaskCanceledException), CreateError},\n      {typeof(TimeoutRejectedException), CreateError},\n      {typeof(BrokenCircuitException), CreateError},\n      {typeof(BrokenCircuitException<HttpResponseMessage>), CreateError},\n  };\n  builder.Services\n      .AddOcelot(builder.Configuration)\n      .AddPolly<MyProvider>(MyErrorMapping);\n  // Note: Default error mapping is defined in the DefaultErrorMapping field of the Ocelot.Provider.Polly.OcelotBuilderExtensions class\n\n\"\"\"\"\n\n.. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\n.. [#f2] If something doesn't work or you're stuck, consider reviewing the current `QoS issues <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+QoS&type=issues>`_ filtered by the |QoS_label| label.\n.. [#f3] The :ref:`Global Configuration <qos-global-configuration>` for dynamic routes was first introduced in pull request `351`_ and released in version `7.0.1`_.\n Since then, global configuration for static routes was added in pull requests `2081`_ and `2339`_, and delivered in version `24.1`_.\n Support for dynamic routes was also added in pull request `2339`_ and delivered in version `24.1`_.\n.. [#f4] The :ref:`Absolute timeout <qos-notes-absolute-timeout>` configuration, used as the :ref:`config-default-timeout`, and the :ref:`config-timeout` feature were requested in issue `1314`_, implemented in pull request `2073`_, and officially released in version `24.1`_.\n.. [#f5] The :ref:`Extensibility <qos-extensibility>` feature was requested in issue `1875`_ and implemented through pull request `1914`_, as part of version `23.2`_.\n\n.. _351: https://github.com/ThreeMammals/Ocelot/pull/351\n.. _1314: https://github.com/ThreeMammals/Ocelot/issues/1314\n.. _1875: https://github.com/ThreeMammals/Ocelot/issues/1875\n.. _1914: https://github.com/ThreeMammals/Ocelot/pull/1914\n.. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073\n.. _2081: https://github.com/ThreeMammals/Ocelot/pull/2081\n.. _2339: https://github.com/ThreeMammals/Ocelot/pull/2339\n.. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1\n.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0\n.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\n.. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0\n"
  },
  {
    "path": "docs/features/ratelimiting.rst",
    "content": "Rate Limiting\r\n=============\r\n\r\n  Feature label: `Rate Limiting <https://github.com/ThreeMammals/Ocelot/labels/Rate%20Limiting>`_\r\n\r\n  Handy articles:\r\n\r\n  * `What is rate limiting? | Microsoft Cloud | Microsoft Learn <https://learn.microsoft.com/en-us/microsoft-cloud/dev/dev-proxy/concepts/what-is-rate-limiting>`_ \r\n  * `Rate Limiting pattern | Azure Architecture Center | Microsoft Learn <https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern>`_\r\n  * `Rate limit an HTTP handler in .NET | .NET | Microsoft Learn <https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter>`_\r\n  * `Rate limiting middleware in ASP.NET Core | Microsoft Learn <https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit>`_\r\n\r\nOcelot implements *rate limiting* [#f1]_ for upstream requests to prevent downstream services from being overwhelmed.\r\n\r\n.. _rl-schema:\r\n\r\n``RateLimitOptions`` Schema\r\n---------------------------\r\n\r\n.. _FileRateLimitByHeaderRule: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRateLimitByHeaderRule.cs\r\n.. _FileGlobalRateLimitByHeaderRule: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs\r\n.. _503 Service Unavailable: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503\r\n\r\n  Class: `FileRateLimitByHeaderRule`_\r\n\r\nAs you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-schema` and :ref:`config-dynamic-route-schema` sections, there is a special ``RateLimitOptions`` object schema for routes:\r\n\r\n.. code-block:: json\r\n\r\n  \"RateLimitOptions\": {\r\n    // rule, partition by\r\n    \"ClientIdHeader\": \"\",\r\n    \"ClientWhitelist\": [\"\"],\r\n    // management opts\r\n    \"EnableRateLimiting\": true,\r\n    \"EnableHeaders\": true,\r\n    // algorithm\r\n    \"Limit\": 1,\r\n    \"Period\": \"\",\r\n    \"Wait\": \"\",\r\n    // extended opts\r\n    \"StatusCode\": 1,\r\n    \"QuotaMessage\": \"\",\r\n    \"KeyPrefix\": \"\"\r\n  }\r\n\r\nAdditionally, the :ref:`config-global-configuration-schema` allows configuring global *Rate Limiting* options.\r\n\r\n  **Note 1**: The complete route-level ``RateLimitOptions`` schema, including all available properties, is defined in the C# `FileRateLimitByHeaderRule`_ class.\r\n  The global ``RateLimitOptions`` schema includes an additional ``RouteKeys`` array option, which allows grouping routes to which the global options will apply (refer to the C# `FileGlobalRateLimitByHeaderRule`_ class for details).\r\n  If the ``RouteKeys`` option is not defined in the global ``RateLimitOptions``, the global settings will apply to all routes.\r\n\r\n  **Note 2**: You do not need to set all of these options due to default values, but the following rule options are required: ``Limit`` and ``Period``.\r\n  If these required options are undefined and no global configuration is present, Ocelot will fail to start due to an internally generated validation error, which will be visible in the logs.\r\n\r\n  **Note 3**: Several :ref:`deprecated options <rl-deprecated-options>` originating from version `24.0`_ and earlier (see `old schema`_) are retained for one release cycle.\r\n  Both introduced and :ref:`deprecated options <rl-deprecated-options>` are detailed in the :ref:`rl-configuration` table below.\r\n\r\n.. _rl-configuration:\r\n\r\nConfiguration [#f2]_\r\n--------------------\r\n\r\nA complete configuration consists of both route-level and global *Rate Limiting*.\r\nYou can configure the following options in the ``GlobalConfiguration`` section of `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n\r\n  \"Routes\": [\r\n    {\r\n      \"Key\": \"R1\",\r\n      \"RateLimitOptions\": {\r\n        \"ClientWhitelist\": [\"ocelot-client1-preshared-key\"],\r\n        \"Limit\": 1000,\r\n        \"Period\": \"20s\", // (milli)seconds, minutes, hours, days\r\n        \"Wait\": \"1.5m\" // (milli)seconds, minutes, hours, days\r\n        \"StatusCode\": 418, // I'm a teapot -> this is special status\r\n        \"QuotaMessage\": \"Out of coffee! Our bar can only serve up to {0} cups of coffee every {1}. In the meantime, why not grab some tea and relax for Retry-After seconds until we're ready to serve again?\"\r\n      }\r\n    }\r\n  ],\r\n  \"GlobalConfiguration\": {\r\n    \"BaseUrl\": \"https://api.ocelot.net\",\r\n    \"RateLimitOptions\": {\r\n      \"RouteKeys\": [\"R1\"], // if undefined or empty array, opts will apply to all routes\r\n      \"ClientIdHeader\": \"Oc-Client\", // std (default) header name\r\n      \"Limit\": 100,\r\n      \"Period\": \"30s\", // ms, s, m, h, d\r\n      \"Wait\": \"1m\", // ms, s, m, h, d\r\n      \"StatusCode\": 429, // Too Many Requests -> standard status\r\n      \"QuotaMessage\": \"Ocelot API calls quota exceeded! Maximum admitted {0} per {1}.\", // standard template with 2 parameters\r\n      \"KeyPrefix\": \"ocelot-rate-limiting\" // for caching key\r\n    }\r\n  }\r\n\r\n.. list-table::\r\n  :widths: 25 75\r\n  :header-rows: 1\r\n\r\n  * - :ref:`Schema <rl-schema>` Option\r\n    - Description\r\n  * - ``ClientIdHeader``\r\n    - Specifies the header used to identify clients, with \"Oc-Client\" set as the default.\r\n  * - ``ClientWhitelist``\r\n    - An array that contains the clients exempt from *rate limiting*.\r\n  * - ``EnableRateLimiting``\r\n    - Enables or disables rate limiting. Defaults to ``true`` (enabled).\r\n  * - ``EnableHeaders``\r\n    - Specifies whether the ``X-RateLimit-*`` and ``Retry-After`` headers are enabled. If undefined, defaults to ``true`` (enabled).\r\n  * - ``Limit``\r\n    - The maximum number of requests a client can make within a given time ``Period``.\r\n  * - ``Period``\r\n    - Rate limiting period (fixed window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d).\r\n      If the exact ``Limit`` of requests is reached (quota exceeded\\*), the request is immediately blocked, and if ``Wait`` is defined, a waiting period begins.\r\n  * - ``Wait``\r\n    - Rate limiting wait window (no servicing period) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d).\r\n      This option can have shorter or longer durations compared to the fixed window duration specified as ``Period``.\r\n      The waiting interval either extends or shortens the Quota Exceeded period\\*, which typically ends after the fixed window elapses.\r\n  * - ``StatusCode``\r\n    - The rejection status code returned during the Quota Exceeded period\\*.\r\n      Default value: 429 (`Too Many Requests`_).\r\n  * - ``QuotaMessage``\r\n    - Specifies the message displayed when the quota is exceeded.\r\n      The value to be used as the formatter for the Quota Exceeded\\* response message.\r\n      If none specified the default will be informative.\r\n  * - ``KeyPrefix``\r\n    - The counter prefix, used to compose the rate limiting counter caching key to be used by the ``IRateLimitStorage`` service.\r\n      Default value: \"Ocelot.RateLimiting\"\r\n\r\n.. admonition:: \"Quota Exceeded period\" term\r\n\r\n  The **Quota Exceeded period** refers to the ``Wait`` window, if defined, or the remaining duration of the fixed ``Period`` following the moment the request ``Limit`` is exceeded.\r\n  During this time, the configured rejection ``StatusCode`` is returned, and the formatted ``QuotaMessage`` is written to the response body.\r\n  To determine when this period ends, clients should inspect the ``Retry-After`` header, which provides a floating-point value indicating the number of seconds until the next allocated fixed window begins.\r\n  The ``X-RateLimit-*`` headers are included in the response during the *Quota Exceeded period*, provided that headers are enabled via the ``EnableHeaders`` option.\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note 1**: If the ``RouteKeys`` option is not defined or the array is empty in the global ``RateLimitOptions``, the global settings will apply to all routes.\r\n  If the array contains route keys, it defines a single group of routes to which the global options apply.\r\n  Routes excluded from this group must specify their own route-level ``RateLimitOptions``.\r\n\r\n  **Note 2**: The string values for the ``Period`` and ``Wait`` options must contain a floating-point number followed by one of the supported time units: 'ms', 's', 'm', 'h', or 'd'.\r\n  If no unit is specified, the value defaults to milliseconds. For example, \"333.5\" is interpreted as 333 milliseconds and 500 microseconds (equivalent to \"333.5ms\").\r\n  The floating-point component may be omitted; for example, \"10.0s\" is equivalent to \"10s\".\r\n  These values are parsed dynamically at runtime, so the required ``Period`` option in `ocelot.json`_ is validated early through fluent validation when the Ocelot app starts.\r\n  If an invalid value is provided, the *Rate Limiting* middleware will throw a ``FormatException``, which is logged accordingly.\r\n\r\n.. _rl-deprecated-options:\r\n\r\nDeprecated options [#f3]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n.. warning::\r\n\r\n  Here are the deprecated options from the `old schema`_:\r\n\r\n  .. list-table::\r\n    :widths: 30 70\r\n    :header-rows: 1\r\n\r\n    * - *Deprecated and Introduced Options*\r\n      - *Description*\r\n    * - ``DisableRateLimitHeaders`` and ``EnableHeaders``\r\n      - Specifies whether the ``X-RateLimit-*`` and ``Retry-After`` headers are disabled.\r\n    * - ``PeriodTimespan`` and ``Wait``\r\n      - This parameter specifies the time, in **seconds**, after which a retry is allowed.\r\n        During this interval, the ``QuotaExceededMessage`` will be included in the response, along with the corresponding ``HttpStatusCode``.\r\n        Clients are encouraged to refer to the ``Retry-After`` header to determine when subsequent requests can be made.\r\n    * - ``HttpStatusCode`` and ``StatusCode``\r\n      - Specifies the HTTP status code returned during *rate limiting*, with a default value of **429** (`Too Many Requests`_).\r\n    * - ``QuotaExceededMessage`` and ``QuotaMessage``\r\n      - Specifies the message displayed when the quota is exceeded. This option is optional, and the default message is informative.\r\n    * - ``RateLimitCounterPrefix`` and ``KeyPrefix``\r\n      - Specifies the counter prefix used to construct the *rate limiting* counter cache key.\r\n\r\nNotes\r\n-----\r\n\r\n.. note::\r\n\r\n  1. Prior to version `24.1`_, global options were only accessible in the special :ref:`Dynamic Routing <routing-dynamic>` mode.\r\n  Since version `24.1`_, global configuration has been available for both static and dynamic routes.\r\n  As a team, we would consider the idea of implementing such a global configuration for aggregated routes.\r\n  However, an aggregated route is essentially a combination of static routes.\r\n\r\n  2. Global *rate limiting* options may not be practical as they apply limits to all routes.\r\n  In a microservices architecture, it is unusual to enforce the same limitations across all routes.\r\n  Configuring per-route *rate limiting* could offer a more tailored solution.\r\n  However, global *rate limiting* can be logical if all routes share the same downstream hosts, thereby restricting the usage of a single service or a single product.\r\n\r\n  3. The ``DisableRateLimitHeaders`` option is deprecated as of version `24.1`_.\r\n  Use ``EnableHeaders`` instead, applying boolean value negation as needed.\r\n  If ``DisableRateLimitHeaders`` is defined, it takes precedence; otherwise, ``EnableHeaders`` will be used.\r\n  Do not define both options.\r\n  This setting is retained for backward compatibility but is subject to change.\r\n  Therefore, the ``DisableRateLimitHeaders`` option will be removed in the upcoming major release, version `25.0`_.\r\n  The same applies to other :ref:`deprecated options <rl-deprecated-options>`.\r\n\r\n  4. Ocelot's own *rate limiting* does not utilize built-in ASP.NET Core features, so it is not based on the \"`Rate limiting middleware in ASP.NET Core`_\" described in the :ref:`rl-roadmap` below.\r\n  The Ocelot team believes that the ASP.NET Core rate limiting middleware enables global limitations through its rate-limiting policies.\r\n\r\n.. _rl-algorithms:\r\n\r\nAlgorithms\r\n----------\r\n\r\nThe currently implemented rate limiter algorithms in Ocelot are:\r\n\r\n- **Fixed window**: Based on the ``Period`` option, without the ``Wait`` option (previously known as the deprecated ``PeriodTimespan``).\r\n- **Hybrid fixed window**: The combination of ``Period`` and ``Wait`` enables fixed-window-like behavior with additional control over the duration and handling of the *\"Quota Exceeded period\"*.\r\n\r\nHistorically, Ocelot's rate limiting algorithm was a hybrid, combining the classic \"fixed window\" approach with a waiting no-service period.\r\nSince version `24.1`_, the hybrid algorithm has been split into two distinct algorithms, allowing the classic \"fixed window\" to be used independently.\r\n\r\nTo understand the terminology, please refer to the Handy Articles listed at the beginning of this chapter.\r\nFor beginners, here is a quick link: `Announcing ASP.NET Core rate limiting algorithms`_.\r\nFor professionals, we recommend reading the official Microsoft Learn article \"`Rate limiting middleware in ASP.NET Core`_\", especially the `Rate Limiter Algorithms`_ section, and/or searching the internet for additional resources.\r\n\r\n  **Note 1**: Ocelot's own rate limiter does not implement other classic algorithms such as \"Sliding Window\", \"Token Bucket\", or \"Concurrency\".\r\n  However, these algorithms are outlined in the :ref:`rl-roadmap`.\r\n\r\n  **Note 2**: Ocelot's own rate limiter does not manage concurrent HTTP requests via a queue.\r\n  Therefore, all concurrency handling and decision-making should be implemented on the client side using classic retry patterns to ensure quality of service.\r\n  The management strategy is deliberately simple: *First-In means First Wins*.\r\n  If the first request acquires a virtual lease from the limiting quota and the quota is immediately exceeded, the second request will be rejected with a 429 `Too Many Requests`_ response.\r\n\r\n.. _Announcing ASP.NET Core rate limiting algorithms: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#what-is-rate-limiting:~:text=There%20are%20multiple%20different%20rate%20limiting%20algorithms%20to%20control%20the%20flow%20of%20requests.\r\n.. _Rate limiter algorithms: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0#rate-limiter-algorithms\r\n.. _Rate limiting middleware in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit\r\n\r\nRules (Partitions)\r\n------------------\r\n\r\n.. _API Key partition: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0#by-api-key\r\n\r\nOcelot's rate limiting *rule* is a superset of the configuration options used to set up rate-limited access to a route.\r\nIt enables partitioned rate limiting by processing the following artifacts through distinct stages: the client's identifier, a dedicated partition counter (quota), rate limiter algorithms, and the quota-exceeded response behavior.\r\n\r\nBy Client's Header\r\n^^^^^^^^^^^^^^^^^^\r\n\r\n  | Class: `FileRateLimitByHeaderRule`_\r\n  | JSON: :ref:`rl-schema`\r\n\r\nCurrently, Ocelot's own rate limiting middleware supports and processes only the *\"By Client's Header\"* rule (partition), commonly referred to as the \"`API Key partition`_\" in ASP.NET Core terminology.\r\nOcelot's rate limiting architecture provides dedicated subpartitions for each route, each with an independent counter for the rate limiter algorithm.\r\nTherefore, when client traffic enters the Ocelot pipeline, the current request is processed as follows:\r\n\r\n1. Ocelot identifies the route by matching the URL path to the upstream route path, and allows the rate limiting middleware to process the client as part of the route partition.\r\n2. Ocelot's rate limiting middleware creates the client's identity based on the configured *\"By Client's Header\"* rule and assigns a dedicated rate limiter counter to that client.\r\n3. The rate limiting middleware executes the configured rate limiter algorithm, specifically the (hybrid) fixed window. Refer to the currently implemented :ref:`rl-algorithms` for details.\r\n4. If the quota is exceeded, the rate limiting middleware returns appropriate \"Quota Exceeded period\" artifacts in the response, such as the status code, body message, and headers including ``Retry-After``.\r\n\r\n.. note::\r\n  If the client is not successfully identified, the rate limiting middleware blocks the request with a `503 Service Unavailable`_ status and writes an appropriate error message to the response body.\r\n  Possible reasons for an empty identity include a missing header or an invalid ``ClientIdHeader`` value, as explained in the warning below.\r\n  Whitelisted clients (defined via the ``ClientWhitelist`` option) are processed without limitation.\r\n\r\n.. warning::\r\n  Ocelot's rate limiting middleware is not responsible for validating API keys, also known as client header values, to be read from the configured header (``ClientIdHeader`` option).\r\n  Users and developers must register these header values as pre-shared API keys on Ocelot's side and ensure they are validated before handing control over to the ``RateLimitingMiddleware``.\r\n\r\n  We recommend implementing a custom middleware to validate API keys (client header values) and injecting it into the Ocelot pipeline using the :doc:`../features/middlewareinjection` feature.\r\n  Specifically, the ``PreErrorResponderMiddleware`` (position 3) should be overridden, as it is invoked before the ``RateLimitingMiddleware`` at position 10.\r\n  A more advanced solution may involve using the ``SecurityMiddleware`` at position 7, but in this case, users must implement their own ``ISecurityPolicy`` service and replace it in the :doc:`../features/dependencyinjection` (DI) container.\r\n  To understand the Ocelot pipeline and its middleware positions, refer to the \":ref:`mi-ocelot-pipeline-builder`\" documentation.\r\n\r\n.. _rl-roadmap:\r\n\r\nRoadmap\r\n-------\r\n\r\n  | Feature label: `Rate Limiting`_\r\n  | Development history: `Rate Limiting <https://github.com/ThreeMammals/Ocelot/pulls?q=is%3Apr+label%3A%22Rate+Limiting%22>`__ [#f4]_\r\n\r\n- **Rules**: The Ocelot team is considering a redesign of the *Rate Limiting* feature in light of the \"`Announcing Rate Limiting for .NET`_\" article by Brennan Conroy, published on July 13th, 2022.\r\n\r\n  .. note::\r\n    Discover the new rate limiting functionality in ASP.NET Core:\r\n\r\n    * The `RateLimiter Class <https://learn.microsoft.com/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter>`_, available since ASP.NET Core 7.0\r\n    * The `System.Threading.RateLimiting <https://www.nuget.org/packages/System.Threading.RateLimiting>`_ NuGet package\r\n    * The `Rate limiting middleware in ASP.NET Core`_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson\r\n\r\n  As of now, the decision has been made to retain Ocelot's own `RateLimitingMiddleware`_ and extend it with an additional rule that will reference the attached ASP.NET Core rate limiting policy.\r\n  This new rule is highly likely to be delivered in version `25.0`_, following the opening of pull request `2188`_.\r\n\r\n- **Algorithms**:\r\n  In addition to the currently implemented hybrid \"Fixed window\" algorithm, which is built into Ocelot, the team plans to introduce other industry-standard algorithms, such as \"Sliding window\", \"Token bucket\", and \"Concurrency, with priority given to \"Sliding window\" as the first.\r\n  These lightweight algorithms should be easily configurable via JSON by end users who are not .NET developers, in order to avoid writing additional C# source code.\r\n  Other interesting algorithms are welcome for discussion.\r\n\r\nWe encourage you to share your thoughts with us in the `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ of the repository. |octocat|\r\nFilter the current discussions by the `Rate Limiting <https://github.com/ThreeMammals/Ocelot/discussions?discussions_q=label%3A%22Rate+Limiting%22>`__ label.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] Historically, the \"`Rate Limiting <#rate-limiting>`__\" feature is one of Ocelot's oldest and first features. This feature was introduced in pull request `37`_ and it was initially released in version `1.3.2`_.\r\n.. [#f2] Global :ref:`Configuration <rl-configuration>` feature was introduced in pull request `2294`_ and delivered in version `24.1`_.\r\n.. [#f3] Several :ref:`deprecated options <rl-deprecated-options>` originating from version `24.0`_ and earlier (see `old schema`_) are retained for one release cycle.\r\n  They are likely to be removed in the upcoming major release, version `25.0`_, which will include a significant upgrade to the *Rate Limiting* feature (refer to the :ref:`rl-roadmap`).\r\n  The Ocelot team plans to implement an automatic configuration upgrade mechanism to support backward compatibility.\r\n  However, we recommend reviewing the updated schema and beginning to adopt the new options.\r\n.. [#f4] Since pull request `37`_ and version `1.3.2`_, the Ocelot team has reviewed and redesigned the *Rate Limiting* feature.\r\n  A fix for bug `1590`_ (pull request `1592`_) was released as part of version `23.3`_ to ensure stable behavior.\r\n  Global :ref:`configuration <rl-configuration>` support was introduced in pull request `2294`_ and delivered in version `24.1`_.\r\n\r\n.. _Announcing Rate Limiting for .NET: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main//samples/Basic/ocelot.json\r\n.. _article by @catcherwong: http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/\r\n.. _Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429\r\n.. _old schema: https://github.com/ThreeMammals/Ocelot/blob/24.0.0/src/Ocelot/Configuration/File/FileRateLimitOptions.cs\r\n.. _RateLimitingMiddleware: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20RateLimitingMiddleware&type=code\r\n\r\n.. _37: https://github.com/ThreeMammals/Ocelot/pull/37\r\n.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590\r\n.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592\r\n.. _2188: https://github.com/ThreeMammals/Ocelot/pull/2188\r\n.. _2294: https://github.com/ThreeMammals/Ocelot/pull/2294\r\n.. _1.3.2: https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2\r\n.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n"
  },
  {
    "path": "docs/features/routing.rst",
    "content": "Routing\r\n=======\r\n\r\nOcelot's primary function is to handle incoming HTTP requests and forward them to a downstream service.\r\nCurrently, Ocelot supports this only through HTTP requests. In the future, it might support other transport mechanisms.\r\n\r\nOcelot defines the process of routing one request to another as a \"Route\".\r\nTo make Ocelot functional, you must set up a *route* in its configuration.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Routes\": []\r\n  }\r\n\r\nTo configure a *route*, you need to add one to the ``Routes`` JSON array.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamHttpMethod\": [ \"Get\", \"Post\" ],\r\n    \"UpstreamPathTemplate\": \"/posts/{postId}\",\r\n    \"DownstreamPathTemplate\": \"/api/posts/{postId}\",\r\n    \"DownstreamScheme\": \"https\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"localhost\", \"Port\": 80 }\r\n    ]\r\n  }\r\n\r\nThe ``DownstreamPathTemplate``, ``DownstreamScheme``, and ``DownstreamHostAndPorts`` properties define the URL to which a request will be forwarded.\r\n\r\nThe ``DownstreamHostAndPorts`` property is a collection that specifies the host and port of downstream services to which you intend to forward requests.\r\nTypically, it contains a single entry; however, in cases where *load balancing* is required, Ocelot allows you to add multiple entries and select an appropriate :doc:`../features/loadbalancer`.\r\n\r\nThe ``UpstreamPathTemplate`` property specifies the URL that Ocelot uses to determine the appropriate ``DownstreamPathTemplate`` for a given request.\r\nThe ``UpstreamHttpMethod`` property enables Ocelot to differentiate between requests with different HTTP verbs directed to the same URL.\r\nYou can either specify a particular list of HTTP methods or leave the list empty to allow all methods.\r\n\r\n  **Note**: The complete schema on a single *route* object is described in the :ref:`config-route-schema` section of the :doc:`../features/configuration` feature.\r\n\r\n.. _routing-placeholders:\r\n\r\nPlaceholders\r\n------------\r\n\r\nIn Ocelot, you can add placeholders for variables to your templates using the format of ``{something}``.\r\nThe placeholder variable must be included in both the ``DownstreamPathTemplate`` and ``UpstreamPathTemplate`` properties.\r\nWhen present, Ocelot attempts to substitute the value of the placeholder from the ``UpstreamPathTemplate`` into the ``DownstreamPathTemplate`` for each request it processes.\r\n\r\nYou can also do a :ref:`routing-catch-all` type of *route* e.g. \r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamHttpMethod\": [ \"Get\", \"Post\" ],\r\n    \"UpstreamPathTemplate\": \"/{everything}\",\r\n    \"DownstreamPathTemplate\": \"/api/{everything}\",\r\n    \"DownstreamScheme\": \"https\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"localhost\", \"Port\": 80 }\r\n    ]\r\n  }\r\n\r\nThis will forward all path and query string combinations to the downstream service, appending them after the ``/api`` path.\r\n\r\n  **Note**: The default routing configuration is **case-insensitive**.\r\n  To change this, you can specify the following setting on a per-route basis:\r\n\r\n  .. code-block:: json\r\n\r\n    \"RouteIsCaseSensitive\": true\r\n\r\n  This means that when Ocelot attempts to match an incoming upstream URL with an upstream template, the evaluation will be *case-sensitive*.\r\n\r\n.. _routing-embedded-placeholders:\r\n\r\nEmbedded Placeholders [#f1]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nBefore version `23.4`_, Ocelot could not evaluate multiple placeholders embedded between two forward slashes (``/``).\r\nAdditionally, it faced difficulties distinguishing placeholders from other elements within the slashes.\r\nFor example, when the pattern ``/{url}-2/`` was applied to ``/y-2/``, it would produce ``{url}`` with ``y-2`` value.\r\n\r\nWe have introduced an improved method for placeholder evaluation, making it easier to identify placeholders in complex URLs.\r\n\r\n**Example**:\r\n\r\n* **Path Pattern**: ``/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}``\r\n* **Upstream URL Path**: ``/api/invoices_super/123-456_abcd/789?urlId=987``\r\n* **Resulting Placeholders**:\r\n\r\n  - ``{url0}`` = ``super``\r\n  - ``{url1}`` = ``123``\r\n  - ``{url2}`` = ``456``\r\n  - ``{url3}`` = ``789``\r\n  - ``{url4}`` = ``987``\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note**, we believe this feature should be compatible with any URL query strings, although it has not been thoroughly tested.\r\n\r\n.. _routing-empty-placeholders:\r\n\r\nEmpty Placeholders [#f2]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nThis represents a special edge case of :ref:`routing-placeholders`, in which the value of the placeholder is simply an empty string (``\"\"``).\r\n\r\nFor example, given the following *route* configuration:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/invoices/{url}\",\r\n    \"DownstreamPathTemplate\": \"/api/invoices/{url}\",\r\n  }\r\n\r\n.. role::  htm(raw)\r\n    :format: html\r\n\r\nThis route works correctly when ``{url}`` is specified. For instance:\r\n\r\n* ``/invoices/123``  :htm:`&rarr;`  ``/api/invoices/123``\r\n\r\n**Edge Cases with Empty Placeholder Values**:\r\n\r\n1. **Empty Placeholder**: When ``{url}`` is empty, the upstream path ``/invoices/`` routes to the downstream path ``/api/invoices/``.\r\n2. **Omitting the Last Slash**: When the trailing slash is omitted, the upstream path ``/invoices`` should still route to the downstream path ``/api/invoices``.\r\n   This behavior aligns intuitively with user expectations.\r\n\r\n.. _routing-catch-all:\r\n\r\nCatch All\r\n---------\r\n\r\nOcelot's *routing* supports a *\"Catch All\"* style, allowing users to specify routes that match all incoming traffic.\r\n\r\nIf you configure your settings as shown below, all requests will be proxied directly.\r\nThe placeholder ``{catchAll}`` is not significant, and any name can be used.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/{catchAll}\",\r\n    \"DownstreamPathTemplate\": \"/{catchAll}\",\r\n    // ...\r\n  }\r\n\r\nThe *\"Catch All\"* route has a lower :ref:`priority <routing-priority>` than other routes.\r\nIf the following route is included in your configuration, Ocelot will match it before the *\"Catch All\"* route.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/\",\r\n    \"DownstreamPathTemplate\": \"/\",\r\n    // ...\r\n  }\r\n\r\n.. _routing-priority:\r\n\r\nPriority [#f3]_\r\n---------------\r\n\r\nYou can define the order in which your *routes* match the upstream URL by including a ``Priority`` property in the `ocelot.json`_ file.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Priority\": 0\r\n  }\r\n\r\nPriority **0** is the lowest *priority*.\r\nOcelot always assigns ``0`` to :ref:`routing-catch-all` routes, and this value is still hardcoded.\r\nBeyond that, you are free to assign any *priority* you wish.\r\n\r\ne.g. you could have\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/goods/{catchAll}\",\r\n    \"Priority\": 0\r\n  }\r\n\r\nand\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/goods/delete\",\r\n    \"Priority\": 1\r\n  }\r\n\r\nIn the example above, if a request is made to Ocelot on ``/goods/delete``, it will match the ``/goods/delete`` route.\r\nPreviously, it would have matched ``/goods/{catchAll}``, as this was the first *route* in the list.\r\n\r\nQuery Placeholders\r\n------------------\r\n\r\nIn addition to URL path :ref:`routing-placeholders`, Ocelot can forward query string parameters, processing them in the form of ``{something}``.\r\nThe query parameter placeholder must be included in both the ``DownstreamPathTemplate`` and ``UpstreamPathTemplate`` properties.\r\nPlaceholder replacement works bi-directionally between paths and query strings, although it is subject to certain restrictions (see :ref:`routing-merging-of-query-parameters`).\r\n\r\nPath to Query String direction\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nOcelot allows you to include a query string as part of the ``DownstreamPathTemplate``, as demonstrated in the following example:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/api/units/{subscription}/{unit}/updates\",\r\n    \"DownstreamPathTemplate\": \"/api/subscriptions/{subscription}/updates?unitId={unit}\",\r\n  }\r\n\r\nIn this example, Ocelot uses the value of the ``{unit}`` placeholder from the upstream path template and includes it in the downstream request as a query string parameter named ``unitId``.\r\n\r\n  **Note**: Ensure that the placeholder is named differently to account for the :ref:`routing-merging-of-query-parameters`.\r\n\r\nQuery String to Path direction\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nOcelot also allows you to include query string parameters in the ``UpstreamPathTemplate``, enabling you to match specific queries to corresponding services:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/api/subscriptions/{subscriptionId}/updates?unitId={uid}\",\r\n    \"DownstreamPathTemplate\": \"/api/units/{subscriptionId}/{uid}/updates\",\r\n  }\r\n\r\nIn this example, Ocelot matches only requests with a corresponding URL path where the query string begins with ``unitId=something``.\r\nAdditional queries are permitted but must follow the matching parameter.\r\nAdditionally, Ocelot replaces the ``{uid}`` parameter in the query string and incorporates it into the downstream request path.\r\n\r\n  **Note**: The best practice is to use a placeholder name that differs from the name of the query parameter to accommodate the :ref:`routing-merging-of-query-parameters`.\r\n\r\n.. _routing-catch-all-query-string:\r\n\r\nCatch All Query String\r\n^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nOcelot's *routing* also supports a \":ref:`routing-catch-all`\" style, allowing all query string parameters to be forwarded.\r\nThe placeholder ``{query}`` is not significant, and any name can be used.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/contracts?{query}\",\r\n    \"DownstreamPathTemplate\": \"/apipath/contracts?{query}\",\r\n  }\r\n\r\nThis query string routing feature is particularly useful in scenarios where the query string needs to be routed without any transformations—for example, OData filters (see issue `1174`_).\r\n\r\n  **Note**: The ``{query}`` placeholder can remain empty while catching all query strings, as this functionality is part of the \":ref:`Empty Placeholders <routing-empty-placeholders>`\" feature [#f2]_.\r\n  Consequently, upstream paths ``/contracts?`` and ``/contracts`` are routed to the downstream path ``/apipath/contracts``, with no query string attached.\r\n\r\n.. _routing-merging-of-query-parameters:\r\n\r\nMerging of Query Parameters\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nQuery string parameters are unsorted and merged to form the final downstream URL.\r\nThis process is crucial because the ``DownstreamUrlCreatorMiddleware`` requires control over placeholder replacement and the merging of duplicate parameters.\r\nA parameter that appears first in the ``UpstreamPathTemplate`` may occupy a different position in the final downstream URL.\r\nMoreover, if the ``DownstreamPathTemplate`` includes query parameters at the beginning, their positions in the ``UpstreamPathTemplate`` will remain undefined unless explicitly specified.\r\n\r\nIn a typical scenario, the merging algorithm constructs the final downstream URL query string as follows:\r\n\r\n1. It begins by taking the initially defined query parameters in the ``DownstreamPathTemplate`` and placing them at the start, along with any necessary placeholder replacements.\r\n2. Next, it adds all parameters from the :ref:`routing-catch-all-query-string`, represented by the placeholder ``{query}``, in the second position—following the explicitly defined parameters from *step 1*.\r\n3. Finally, it appends any remaining replaced placeholder values as parameter values to the end of the query string, if present.\r\n\r\nArray parameters in ASP.NET API's model binding\r\n\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\r\n\r\nDue to the merging of parameters, ASP.NET API's special `model binding`_ for arrays does not support the array item representation format ``selectedCourses=1050&selectedCourses=2000``.\r\nThis query string will be merged into ``selectedCourses=1050`` in the downstream URL, leading to the loss of array data.\r\nIt is essential for upstream clients to generate the correct query string for array models, such as ``selectedCourses[0]=1050&selectedCourses[1]=2000``.\r\nFor a detailed explanation of array model binding, refer to the documentation: \"`Bind arrays and string values from headers and query strings`_\".\r\n\r\nControl over parameter existence\r\n\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\r\n\r\nBe aware that query string placeholders are subject to naming restrictions due to the implementation of the ``DownstreamUrlCreatorMiddleware`` merging algorithm.\r\nNevertheless, this restriction also offers flexibility in managing the presence of parameters in the final downstream URL based on their names.\r\n\r\nConsider the following two development scenarios :htm:`&rarr;`\r\n\r\n1. A developer wishes **to preserve a parameter** after substituting a placeholder (refer to issue `473`_).\r\n   This requires the use of the following template definition:\r\n\r\n   .. code-block:: json\r\n  \r\n     {\r\n       \"UpstreamPathTemplate\": \"/path/{serverId}/{action}\",\r\n       \"DownstreamPathTemplate\": \"/path2/{action}?server={serverId}\"\r\n     }\r\n\r\n   | In this case, the ``{serverId}`` placeholder and the server parameter **names differ**. As a result, the ``server`` parameter is retained.\r\n   | It is important to note that, due to the case-sensitive comparison of names, the ``server`` parameter will not be preserved with the ``{server}`` placeholder. However, using the ``{Server}`` placeholder is acceptable for retaining the parameter.\r\n\r\n2. The developer intends **to remove an outdated parameter** after substituting a placeholder (refer to issue `952`_).\r\n   To achieve this, identical names must be used, adhering to case-sensitive comparison rules.\r\n\r\n   .. code-block:: json\r\n  \r\n     {\r\n       \"UpstreamPathTemplate\": \"/users?userId={userId}\",\r\n       \"DownstreamPathTemplate\": \"/persons?personId={userId}\"\r\n     }\r\n\r\n   | Thus, the ``{userId}`` placeholder and the ``userId`` parameter **have identical names**. As a result, the ``userId`` parameter is eliminated.\r\n   | Be aware that, due to the case-sensitive nature of the comparison, the ``userId`` parameter will not be removed if the ``{userid}`` placeholder is used.\r\n\r\n.. _routing-upstream-host:\r\n\r\nUpstream Host [#f4]_\r\n--------------------\r\n\r\nThis feature allows you to define routes based on the *upstream host*.\r\nIt works by examining the ``Host`` header used by the client and incorporating it into the information used to identify a *route*.\r\n\r\nIn order to use this feature, add the following to your configuration:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"UpstreamHost\": \"mydomain.com\"\r\n  }\r\n\r\nThe *route* above will only match requests where the ``Host`` header value is ``mydomain.com``.\r\n\r\nIf you do not set the ``UpstreamHost`` on a *route*, any ``Host`` header will match it.\r\nAs a result, if you have two routes that are identical except for the ``UpstreamHost``, where one is null and the other is set, Ocelot will prioritize the one that is set.\r\n\r\n.. _routing-upstream-headers:\r\n\r\nUpstream Headers [#f5]_\r\n-----------------------\r\n\r\nIn addition to routing by ``UpstreamPathTemplate``, you can also define ``UpstreamHeaderTemplates``.\r\nFor a *route* to match, all headers specified in this dictionary object must be included in the request headers.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 3\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/\",\r\n    \"UpstreamHeaderTemplates\": { // dictionary\r\n      \"country\": \"uk\", // 1st header\r\n      \"version\": \"v1\"  // 2nd header\r\n    }\r\n  }\r\n\r\nIn this scenario, the *route* matches only if a request contains both headers with the specified values.\r\n\r\nHeader placeholders\r\n^^^^^^^^^^^^^^^^^^^\r\n\r\nLet's explore a more interesting scenario where placeholders can be effectively utilized within your ``UpstreamHeaderTemplates``.\r\n\r\nConsider the following approach using the special placeholder format ``{header:placeholdername}``:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    // downstream opts...\r\n    \"DownstreamPathTemplate\": \"/{versionnumber}/api\", // with placeholder\r\n    // upstream opts...\r\n    \"UpstreamHeaderTemplates\": {\r\n      \"version\": \"{header:versionnumber}\" // 'header:' prefix vs placeholder\r\n    }\r\n  }\r\n\r\nIn this scenario, the entire value of the request header ``version`` is inserted into the ``DownstreamPathTemplate``.\r\nIf needed, a more complex upstream header template can be specified using placeholders such as ``version-{header:version}_country-{header:country}``.\r\n\r\n  **Note 1**: Placeholders are not required in the ``DownstreamPathTemplate``. This scenario can be used to enforce a specific header, regardless of its value.\r\n\r\n  **Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/aggregation` as well.\r\n\r\n.. _routing-security-options:\r\n\r\nSecurity Options [#f6]_\r\n-----------------------\r\n\r\nOcelot facilitates the management of multiple patterns for allowed and blocked IPs using the `IPAddressRange <https://github.com/jsakamoto/ipaddressrange>`_ package, which is licensed under the `MPL-2.0 license <https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE>`_.\r\n\r\nThis feature is designed to enhance IP management, allowing for the inclusion or exclusion of a broad IP range using CIDR notation or specific IP ranges.\r\nThe current managed patterns are as follows:\r\n\r\n.. list-table::\r\n    :widths: 35 65\r\n    :header-rows: 1\r\n\r\n    * - *IP Rule*\r\n      - *Example*\r\n    * - Single IP\r\n      - ``192.168.1.1``\r\n    * - IP Range\r\n      - ``192.168.1.1-192.168.1.250``\r\n    * - IP Short Range\r\n      - ``192.168.1.1-250``\r\n    * - IP Subnet\r\n      - ``192.168.1.0/255.255.255.0``\r\n    * - CIDR IPv4\r\n      - ``192.168.1.0/24``\r\n    * - CIDR IPv6\r\n      - ``fe80::/10``\r\n\r\nHere is a simple example:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"SecurityOptions\": { \r\n      \"IPBlockedList\": [ \"192.168.0.0/23\" ], \r\n      \"IPAllowedList\": [\"192.168.0.15\", \"192.168.1.15\"], \r\n      \"ExcludeAllowedFromBlocked\": true \r\n    }\r\n  }\r\n\r\nPlease **note**:\r\n\r\n* The allowed/blocked lists are evaluated during configuration loading.\r\n* The ``ExcludeAllowedFromBlocked`` property enables specifying a wide range of blocked IP addresses while allowing a subrange of IP addresses. Default value: ``false``.\r\n* The absence of a property in *Security Options* is permitted, as it takes the default value.\r\n* *Security Options* can be configured *globally* in the ``GlobalConfiguration`` JSON [#f7]_. However, they are ignored if overriding options are specified at the route level.\r\n\r\n.. _routing-dynamic:\r\n\r\nDynamic Routing [#f8]_\r\n----------------------\r\n\r\nThe concept of dynamic *routing* allows you to use a :doc:`../features/servicediscovery` provider, eliminating the need to manually configure *route* settings.\r\nFor more details, refer to the :ref:`Dynamic Routing <sd-dynamic-routing>` complete reference in the \":doc:`../features/servicediscovery`\" chapter.\r\n\r\nErrors and Gotchas\r\n------------------\r\n.. _Show and tell: https://github.com/ThreeMammals/Ocelot/discussions/categories/show-and-tell\r\n.. _499 (Client Closed Request): https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status499clientclosedrequest\r\n.. _503 (Service Unavailable): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503\r\n\r\nIn this section, Ocelot team has gathered user scenarios where routing behavior was unclear or errors appeared in the logs.\r\nPlease note that the failed routing cases listed below do not represent all application configurations.\r\nIf your case is not included, feel free to open a \"`Show and tell`_\" discussion.\r\n\r\n* **Magic 499 status**.\r\n  According to Ocelot Core's design, HTTP status code `499 (Client Closed Request)`_ is returned in cases involving an ``OperationCanceledException``.\r\n  Please note that due to extensive warning-level logging, you may encounter spikes in ``499`` responses—as discussed in thread `2072`_.\r\n  This status is typically caused by:\r\n\r\n  A) Forced cancellation of the request by the client\r\n  B) Browser events such as page refreshes or closures while the downstream request is still in progress\r\n\r\n  As a quick recipe, the Ocelot team recommends ensuring client stability and, if necessary, adjusting the :ref:`config-timeout` strategy:\r\n  either increasing or decreasing the route :ref:`config-timeout` depending on your usage scenario and the behavior of the downstream service.\r\n\r\n* **Timeout errors aka 503 status**.\r\n  According to Ocelot Core's design, HTTP status code `503 (Service Unavailable)`_ is returned in cases involving a ``TimeoutException``.\r\n  This status is typically caused by:\r\n\r\n  A) Slow downstream services that may fail to respond\r\n  B) Large requests forwarded to slow downstream services\r\n\r\n  As a quick recipe, the Ocelot team recommends increasing the route :ref:`config-timeout` in your configuration.\r\n  This adjustment can help resolve timeout-related issues with sluggish downstream services, ultimately reducing occurrences of `503 (Service Unavailable)`_.\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note**: For comprehensive documentation regarding errors and status codes in Ocelot, please refer to the :doc:`../features/errorcodes` chapter.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The \":ref:`Embedded Placeholders <routing-embedded-placeholders>`\" feature was requested as part of issue `2199`_ , and released in version `23.4`_\r\n.. [#f2] The \":ref:`Empty Placeholders <routing-empty-placeholders>`\" feature is available starting in version `23.0`_, see issue `748`_ and the `23.0`_ release notes for details.\r\n.. [#f3] The \":ref:`Priority <routing-priority>`\" feature was requested as part of issue `270`_, and released in version `5.0.1`_\r\n.. [#f4] The \":ref:`Upstream Host <routing-upstream-host>`\" feature was requested as part of issue `209`_ (pull request `216`_), and released in version `3.0.1`_\r\n.. [#f5] The \":ref:`Upstream Headers <routing-upstream-headers>`\" feature was proposed in issue `360`_ (pull request `1312`_), and released in version `23.3`_.\r\n.. [#f6] The \":ref:`Security Options <routing-security-options>`\" feature was requested as part of issue `628`_ (version `12.0.1`_), then redesigned and improved by issue `1400`_ (version `23.4.1`_), and published in version `20.0`_ docs.\r\n.. [#f7] Global \":ref:`Security Options <routing-security-options>`\" feature was requested as part of issue `2165`_ , and released in version `23.4.1`_.\r\n.. [#f8] The \":ref:`Dynamic Routing <routing-dynamic>`\" feature was requested as part of issue `340`_, and released in version `7.0.1`_.\r\n  Refer to complete reference in the \":doc:`../features/servicediscovery`\" chapter: :ref:`Dynamic Routing <sd-dynamic-routing>`.\r\n\r\n.. _model binding: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-8.0#collections\r\n.. _Bind arrays and string values from headers and query strings: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#bind-arrays-and-string-values-from-headers-and-query-strings\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n\r\n.. _209: https://github.com/ThreeMammals/Ocelot/issues/209\r\n.. _216: https://github.com/ThreeMammals/Ocelot/pull/216\r\n.. _270: https://github.com/ThreeMammals/Ocelot/issues/270\r\n.. _340: https://github.com/ThreeMammals/Ocelot/issues/340\r\n.. _360: https://github.com/ThreeMammals/Ocelot/issues/360\r\n.. _473: https://github.com/ThreeMammals/Ocelot/issues/473\r\n.. _628: https://github.com/ThreeMammals/Ocelot/issues/628\r\n.. _748: https://github.com/ThreeMammals/Ocelot/issues/748\r\n.. _952: https://github.com/ThreeMammals/Ocelot/issues/952\r\n.. _1174: https://github.com/ThreeMammals/Ocelot/issues/1174\r\n.. _1312: https://github.com/ThreeMammals/Ocelot/pull/1312\r\n.. _1400: https://github.com/ThreeMammals/Ocelot/issues/1400\r\n.. _2072: https://github.com/ThreeMammals/Ocelot/discussions/2072\r\n.. _2165: https://github.com/ThreeMammals/Ocelot/issues/2165\r\n.. _2199: https://github.com/ThreeMammals/Ocelot/issues/2199\r\n\r\n.. _3.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/3.0.1\r\n.. _5.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/5.0.1\r\n.. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1\r\n.. _12.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/12.0.1\r\n.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0\r\n.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _23.4: https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.0\r\n.. _23.4.1: https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.1\r\n"
  },
  {
    "path": "docs/features/servicediscovery.rst",
    "content": "Service Discovery\r\n=================\r\n\r\nOcelot allows you to specify a *service discovery* provider, which it uses to determine the host and port for the downstream service to which it forwards requests.\r\nCurrently, this feature is only supported in the ``GlobalConfiguration`` section.\r\nThis means the same *service discovery* provider is applied to all routes where a ``ServiceName`` is specified at the route level.\r\n\r\n.. _sd-consul:\r\n\r\nConsul\r\n------\r\n\r\n.. _Consul: https://www.consul.io/\r\n.. _Ocelot.Provider.Consul: https://www.nuget.org/packages/Ocelot.Provider.Consul\r\n\r\n  | Package: `Ocelot.Provider.Consul`_\r\n  | Namespace: ``Ocelot.Provider.Consul``\r\n\r\nThe first step is to install `the package <https://www.nuget.org/packages/Ocelot.Provider.Consul>`_, which adds `Consul`_ support to Ocelot:\r\n\r\n.. code-block:: powershell\r\n\r\n    Install-Package Ocelot.Provider.Consul\r\n\r\nTo register *Consul* services, invoke the ``AddConsul()`` extension method using the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_.\r\nInclude the following code in your `Program`_:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.Provider.Consul;\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddConsul(); // or .AddConsul<T>()\r\n\r\nCurrently, there are two types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``.\r\nThe default provider is ``Consul``.\r\nIf the ``ConsulProviderFactory`` cannot read, understand, or parse the ``Type`` property of the ``ServiceProviderConfiguration`` object, a :ref:`sd-consul-provider` instance is created by the factory.\r\n\r\nExplore these types of *service discovery* providers and learn about their differences in the subsections: :ref:`sd-consul-provider` and :ref:`sd-pollconsul-provider`.\r\n\r\n  **Note**: We have made the :ref:`sd-consul-provider` the default *service discovery* provider in Ocelot.\r\n\r\n.. _sd-consul-configuration-in-kv:\r\n\r\nConfiguration in `KV Store`_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nAdd the following when registering your services. Ocelot will attempt to store and retrieve its :doc:`../features/configuration` in the *Consul* `KV Store`_:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 4\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddConsul()\r\n      .AddConfigStoredInConsul();\r\n\r\nYou also need to add the following to your `ocelot.json`_ file.\r\nThis allows Ocelot to locate your *Consul* agent and handle configuration loading and storage from *Consul*.\r\n\r\n.. code-block:: json\r\n\r\n  \"GlobalConfiguration\": {\r\n    \"ServiceDiscoveryProvider\": {\r\n      \"Host\": \"localhost\",\r\n      \"Port\": 9500\r\n    }\r\n  }\r\n\r\nThe team decided to create this feature after working on the `Raft consensus <https://github.com/ThreeMammals/Ocelot.Provider.Rafty>`_ algorithm and realizing how challenging it was.\r\nWhy not take advantage of the fact that `Consul`_ already provides this functionality?\r\nWe believe this means that, to use Ocelot to its fullest potential, you currently need to adopt *Consul* as a dependency.\r\n\r\n  **Note**: This feature has a `3-second TTL`_ cache before it makes a new request to your local *Consul* agent.\r\n\r\n.. _sd-consul-configuration-key:\r\n\r\nConfiguration Key [#f2]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nIf you are using *Consul* for :doc:`../features/configuration` (or other providers in the future), you may want to assign keys to your configurations.\r\nThis allows you to manage multiple configurations.\r\n\r\nIn order to specify the key, you need to set the ``ConfigurationKey`` property in the ``ServiceDiscoveryProvider`` options of the configuration JSON file.\r\nFor example:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 5\r\n\r\n  \"GlobalConfiguration\": {\r\n    \"ServiceDiscoveryProvider\": {\r\n      \"Host\": \"localhost\",\r\n      \"Port\": 9500,\r\n      \"ConfigurationKey\": \"Ocelot_A\"\r\n    }\r\n  }\r\n\r\nIn this example, Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in *Consul*.\r\nIf you do not set the ``ConfigurationKey``, Ocelot will default to using the string ``InternalConfiguration`` as the key.\r\n\r\n.. _sd-consul-provider:\r\n\r\n``Consul`` Provider\r\n^^^^^^^^^^^^^^^^^^^\r\n\r\n  Class: `Ocelot.Provider.Consul.Consul <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+Consul&type=code>`_\r\n\r\nThe following is required in the ``GlobalConfiguration`` section.\r\nThe ``ServiceDiscoveryProvider`` property is mandatory.\r\nIf you do not specify a host and port, the default `Consul`_ values will be used.\r\n\r\n  **Note**: The ``Scheme`` option defaults to HTTP. This was introduced in pull request `1154`_ and defaults to ``http`` to avoid introducing a breaking change.\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 5\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Scheme\": \"https\",\r\n    \"Host\": \"localhost\",\r\n    \"Port\": 8500,\r\n    \"Type\": \"Consul\"\r\n  }\r\n\r\nIn the future, we may add a feature that allows route-specific configuration.\r\n\r\nTo instruct Ocelot that a route should use the *service discovery* provider for its host and port, you need to specify the ``ServiceName`` and the load balancer you wish to use for downstream requests.\r\nCurrently, Ocelot supports the `RoundRobin <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20RoundRobin&type=code>`_ and `LeastConnection <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+LeastConnection&type=code>`_ algorithms.\r\nIf no load balancer is specified, Ocelot will not perform load balancing for requests.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"ServiceName\": \"product\",\r\n    \"LoadBalancerOptions\": {\r\n      \"Type\": \"LeastConnection\"\r\n    }\r\n  }\r\n\r\nWhen set up, Ocelot will look up the downstream host and port from the *service discovery* provider and balance requests across available services.\r\n\r\n.. _sd-pollconsul-provider:\r\n\r\n``PollConsul`` Provider\r\n^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n  Class: `Ocelot.Provider.Consul.PollConsul <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20PollConsul&type=code>`_\r\n\r\nA lot of users have requested a feature where Ocelot *polls Consul* for the latest service information instead of doing so per request.\r\nIf you want Ocelot to *poll Consul* for the latest services, rather than relying on the default behavior (per request), you need to configure the following options:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 4-5\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Host\": \"localhost\",\r\n    \"Port\": 8500,\r\n    \"Type\": \"PollConsul\",\r\n    \"PollingInterval\": 100 // ms\r\n  }\r\n\r\nThe polling interval, measured in milliseconds, specifies how frequently Ocelot calls `Consul`_ for service configuration updates.\r\n\r\n  **Note**: There are trade-offs to consider.\r\n  If you *poll Consul*, Ocelot may not detect if a service is down, depending on your polling interval.\r\n  This could result in more errors compared to retrieving the latest services per request.\r\n  The impact largely depends on the volatility of your services.\r\n  For most users, this is unlikely to be a significant concern, and polling may offer a slight performance improvement over querying `Consul`_ per request (as a sidecar agent).\r\n  However, if you are communicating with a remote `Consul`_ agent, polling provides a more noticeable performance improvement.\r\n\r\nService Definition\r\n^^^^^^^^^^^^^^^^^^\r\n\r\nYour services need to be added to Consul in a manner similar to the example below (C# style, but hopefully it makes sense).\r\nThe key point to note is to avoid including ``http`` or ``https`` in the ``Address`` field.\r\nWe have received feedback regarding issues with the scheme being included in the ``Address``.\r\nAfter reviewing the \"`Agents Overview <https://developer.hashicorp.com/consul/docs/agent>`_\" and \"`Define services <https://developer.hashicorp.com/consul/docs/services/usage/define-services>`_\" documentation, we believe the **scheme** should not be included.\r\n\r\nIn C#\r\n\r\n.. code-block:: csharp\r\n\r\n    new AgentService()\r\n    {\r\n        ID = \"some-id\",\r\n        Service = \"some-service-name\",\r\n        Address = \"localhost\",\r\n        Port = 8080,\r\n    }\r\n\r\nOr, in JSON\r\n\r\n.. code-block:: json\r\n\r\n  \"Service\": {\r\n    \"ID\": \"some-id\",\r\n    \"Service\": \"some-service-name\",\r\n    \"Address\": \"localhost\",\r\n    \"Port\": 8080\r\n  }\r\n\r\nACL Token\r\n^^^^^^^^^\r\n\r\nIf you are using `ACL <https://developer.hashicorp.com/consul/commands/acl/token>`_ with *Consul*, Ocelot supports adding the ``X-Consul-Token`` header.\r\nTo enable this functionality, you must add the following option:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 5\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Host\": \"localhost\",\r\n    \"Port\": 8500,\r\n    \"Type\": \"Consul\",\r\n    \"Token\": \"my-token\"\r\n  }\r\n\r\nOcelot will add this token to the *Consul* client it uses for making requests, and this token will be applied to all subsequent requests.\r\n\r\n.. _sd-consul-service-builder:\r\n\r\nConsul Service Builder [#f3]_\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n  | Interface: ``IConsulServiceBuilder``\r\n  | Implementation: ``DefaultConsulServiceBuilder``\r\n\r\nThe Ocelot community has consistently reported issues with *Consul* services, both in the past and present, such as connectivity problems due to varying *Consul* agent definitions.\r\nSome DevOps engineers prefer grouping services as *Consul* `catalog nodes`_ by customizing the assignment of hostnames to node names, while others prioritize defining agent services using pure IP addresses as hosts, which is linked to the `954`_-bug dilemma.\r\n\r\nSince version `13.5.2`_, the process for constructing the downstream host and port in pull request `909`_ has been changed to prioritize the node name as the host over the agent service address IP.\r\nThis may raise some criticism from the community.\r\n\r\nVersion `23.3`_ introduced a customization feature that enables control over the service-building process through the ``DefaultConsulServiceBuilder`` class.\r\nThis class includes virtual methods that developers and DevOps teams can override to suit their specific requirements.\r\n\r\nThe current logic in the ``DefaultConsulServiceBuilder`` class is as follows:\r\n\r\n.. code-block:: csharp\r\n\r\n  protected virtual string GetDownstreamHost(ServiceEntry entry, Node node)\r\n      => node != null ? node.Name : entry.Service.Address;\r\n\r\nSome DevOps engineers choose to disregard node names, opting for abstract identifiers instead of actual hostnames.\r\nHowever, our team strongly recommends assigning real hostnames or IP addresses to node names, considering this a best practice.\r\nIf this approach does not align with your needs, or if you prefer not to invest time in detailing nodes for downstream services, you could define agent services without node names.\r\nIn such cases, within a *Consul* setup, you would need to override the behavior of the ``DefaultConsulServiceBuilder`` class.\r\nFor further information, refer to the \":ref:`sd-addconsul-generic-method`\" section below.\r\n\r\n.. _sd-addconsul-generic-method:\r\n\r\n``AddConsul<T>`` method\r\n\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\r\n\r\n  Signature: ``IOcelotBuilder AddConsul<TServiceBuilder>(this IOcelotBuilder builder)``\r\n\r\nOverriding the ``DefaultConsulServiceBuilder`` behavior involves two steps:\r\ncreating a new class that inherits from the ``IConsulServiceBuilder`` interface, and injecting this new behavior into the DI container using the ``AddConsul<TServiceBuilder>()`` helper.\r\nHowever, the fastest and most streamlined approach is to inherit directly from the ``DefaultConsulServiceBuilder`` class, as it provides greater flexibility.\r\n\r\nFirst, define a new service-building class:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.Logging;\r\n  using Ocelot.Provider.Consul;\r\n  using Ocelot.Provider.Consul.Interfaces;\r\n\r\n  public class MyConsulServiceBuilder : DefaultConsulServiceBuilder\r\n  {\r\n      public MyConsulServiceBuilder(IHttpContextAccessor contextAccessor, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory)\r\n          : base(contextAccessor, clientFactory, loggerFactory) { }\r\n\r\n      // Use the agent service IP address as the downstream hostname\r\n      protected override string GetDownstreamHost(ServiceEntry entry, Node node)\r\n          => entry.Service.Address;\r\n  }\r\n\r\nNext, inject the new behavior into the DI container, as shown in the Ocelot-Consul setup:\r\n\r\n.. code-block:: csharp\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddConsul<MyConsulServiceBuilder>();\r\n\r\nRefer to the repository's `acceptance test`_ for further examples.\r\n\r\n.. _sd-eureka:\r\n\r\nEureka [#f4]_\r\n-------------\r\n\r\n.. _Steeltoe: https://steeltoe.io\r\n.. _Pivotal: https://pivotal.io/platform\r\n.. _Eureka: https://www.nuget.org/packages/Steeltoe.Discovery.Eureka\r\n.. _Ocelot.Provider.Eureka: https://www.nuget.org/packages/Ocelot.Provider.Eureka\r\n\r\n  | Package: `Ocelot.Provider.Eureka`_\r\n  | Namespace: ``Ocelot.Provider.Eureka``\r\n\r\nThis feature supports the Netflix `Eureka`_ *service discovery* provider.\r\nThe primary reason for this is that it is a key product of `Steeltoe`_, which is associated with `Pivotal`_.\r\nNow, enough of the background!\r\n\r\nThe first step is to install `the package <https://www.nuget.org/packages/Ocelot.Provider.Eureka>`__ that provides `Eureka`_ support for Ocelot:\r\n\r\n.. code-block:: powershell\r\n\r\n    Install-Package Ocelot.Provider.Eureka\r\n\r\nNext, add the following to your `Program <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Eureka/ApiGateway/Program.cs>`__:\r\n\r\n.. code-block:: csharp\r\n\r\n  using Ocelot.Provider.Eureka;\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddEureka();\r\n\r\nFinally, to enable this setup, include the following in your `ocelot.json <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Eureka/ApiGateway/ocelot.json>`__ file:\r\n\r\n.. code-block:: json\r\n\r\n  \"ServiceDiscoveryProvider\": {\r\n    \"Type\": \"Eureka\"\r\n  }\r\n\r\nFollowing the guide `here <https://docs.steeltoe.io/>`_, you may also need to add some configurations to `appsettings.json <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Eureka/ApiGateway/appsettings.json>`_.\r\nFor example, the JSON below informs the `Steeltoe`_ / `Pivotal`_ services where to locate the service discovery server and whether the service should register with it:\r\n\r\n.. code-block:: json\r\n\r\n  \"eureka\": {\r\n    \"client\": {\r\n      \"serviceUrl\": \"http://localhost:8761/eureka/\",\r\n      \"shouldRegisterWithEureka\": false,\r\n      \"shouldFetchRegistry\": true\r\n    }\r\n  }\r\n\r\nIf ``shouldRegisterWithEureka`` is set to ``false``, ``shouldFetchRegistry`` will default to ``true``, so you do not need to set it explicitly; however, it has been included here for clarity.\r\n\r\nOcelot will now register all necessary services during startup and, if the JSON above is provided, it will register itself with *Eureka*.\r\nOne of the services polls *Eureka* every 30 seconds (default) to retrieve the latest service state and persists this information in memory.\r\nWhen Ocelot requests a given service, it retrieves the data from memory, minimizing performance issues.\r\n\r\nIf not explicitly specified in `ocelot.json <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Eureka/ApiGateway/ocelot.json>`__, Ocelot will use the scheme (``http``, ``https``) set in *Eureka*.\r\n\r\n.. _sd-service-fabric:\r\n\r\nService Fabric\r\n--------------\r\n\r\n.. _Service Fabric: https://azure.microsoft.com/en-us/products/service-fabric/\r\n.. _Microsoft.ServiceFabric: https://www.nuget.org/packages/Microsoft.ServiceFabric\r\n\r\nIf you have services deployed in Azure `Service Fabric`_, you typically use the naming service to access them.\r\n\r\nPlease refer to the :doc:`../features/servicefabric` chapter for the complete *essential* documentation.\r\n\r\n  **Note**: Currently, the ``ServiceFabric`` *service discovery* provider is tightly coupled with Ocelot core interfaces, making it a part of Ocelot Core and implemented as the ``ServiceFabricServiceDiscoveryProvider`` class.\r\n  At present, there is no Ocelot extension package that integrates with the `Microsoft.ServiceFabric`_ package or any other relevant package.\r\n  However, the Ocelot team plans to address this in future development, as we believe `Service Fabric`_ is an essential and popular product in the .NET and Azure development world.\r\n  If anyone in the Ocelot community is a professional Azure developer with extensive `Service Fabric`_ experience, please contact our development team directly via GitHub or email.\r\n\r\n.. _sd-dynamic-routing:\r\n\r\nDynamic Routing [#f5]_\r\n----------------------\r\n\r\nThe idea is to enable *dynamic routing* mode when using a *service discovery* provider.\r\nIn this mode, Ocelot uses the first segment of the upstream path to look up the downstream service via the *service discovery* provider.\r\n\r\nAn example of this would be calling Ocelot with a URL like\r\n\r\n* ``https://api.ocelot.net/product/products``\r\n\r\nOcelot will take the first segment of the path, which is ``product``, and use it as a key to look up the service in :ref:`sd-consul`.\r\nIf :ref:`sd-consul-provider` returns a service, Ocelot will request it using the host and port provided by `Consul`_, appending the remaining path segments—in this case, ``products``—to construct final downstream URL:\r\n\r\n* ``http://hostfromconsul:portfromconsul/products``\r\n\r\nOcelot will append any query string to the downstream URL as usual.\r\n\r\n.. warning::\r\n\r\n  To enable *dynamic routing*, the `ocelot.json`_ configuration must contain no static routes in the ``Routes`` collection!\r\n  Currently, dynamic routes and static routes cannot be mixed.\r\n  Additionally, you need to specify the details of the *service discovery* provider as outlined above, along with the downstream ``http(s)`` scheme under ``DownstreamScheme``.\r\n\r\n.. note::\r\n\r\n  In addition to the global ``ServiceDiscoveryProvider`` section, the :ref:`config-global-configuration-schema` includes configurable options such as ``DownstreamScheme``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, and ``Timeout``.\r\n  These options are applicable to all dynamic routes, globally.\r\n  Moreover, starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports these options for overriding global settings.\r\n\r\nFor instance, when exposing Ocelot publicly over HTTPS while routing to internal services over HTTP, your configuration may resemble the following:\r\n\r\n  .. code-block:: json\r\n\r\n    {\r\n      \"Routes\": [], // must be empty to enable dynamic routing!\r\n      \"DynamicRoutes\": [\r\n        // overriding goes here\r\n      ],\r\n      \"GlobalConfiguration\": {\r\n        \"BaseUrl\": \"https://api.ocelot.net\",\r\n        \"DownstreamScheme\": \"http\", // default scheme for all internal services, no SSL\r\n        \"ServiceDiscoveryProvider\": {\r\n          \"Host\": \"localhost\", // if Consul is hosted on the same machine as Ocelot\r\n          \"Port\": 8500,\r\n          \"Type\": \"Consul\",\r\n          \"Namespace\": \"\" // not supported for Consul, but supported for Kubernetes\r\n        },\r\n        \"CacheOptions\": {\r\n          \"TtlSeconds\": 300 // 5 minutes\r\n        },\r\n        \"HttpHandlerOptions\": {\r\n          \"AllowAutoRedirect\": false,\r\n          \"UseCookieContainer\": false,\r\n          \"UseTracing\": false\r\n        },\r\n        \"LoadBalancerOptions\": {\r\n          \"Type\": \"LeastConnection\"\r\n        },\r\n        \"QoSOptions\": {\r\n          \"MinimumThroughput\": 2,\r\n          \"BreakDuration\": 333,\r\n          \"Timeout\": 3000 // ms\r\n        },\r\n        \"RateLimitOptions\": {\r\n          \"ClientIdHeader\": \"Oc-DynamicRouting-Client\",\r\n          \"QuotaMessage\": \"No Quota!\",\r\n          \"StatusCode\": 499 // special shared status\r\n        }\r\n      }\r\n    }\r\n\r\n.. _sd-dynamic-routing-configuration:\r\n\r\nConfiguration [#f6]_\r\n^^^^^^^^^^^^^^^^^^^^\r\n\r\nOcelot also allows configuration of a ``DynamicRoutes`` collection consisting of :ref:`config-dynamic-route-schema` objects.\r\nThis enables overriding ``RateLimitOptions`` for each downstream service, along with other schema-level overrides.\r\nDynamic route options are particularly useful when there are multiple services—such as a '``product``' service and a '``search``' service—and stricter rate limits need to be applied to one over the other.\r\nThe final configuration looks like:\r\n\r\n  .. code-block:: json\r\n\r\n    {\r\n      \"DynamicRoutes\": [\r\n        {\r\n          \"ServiceName\": \"product\",\r\n          \"ServiceNamespace\": \"\", // not supported for Consul, but supported for Kubernetes\r\n          \"RateLimitOptions\": {\r\n            \"Limit\": 5,\r\n            \"Period\": \"1s\",\r\n            \"Wait\": \"1.5s\" // hybrid fixed window\r\n          }\r\n        },\r\n        {\r\n          \"ServiceName\": \"notification\",\r\n          \"CacheOptions\": {\r\n            \"TtlSeconds\": 0 // disable cache for notifying\r\n          },\r\n          \"HttpHandlerOptions\": {\r\n            \"UseTracing\": false // disable tracing\r\n          },\r\n          \"LoadBalancerOptions\": {\r\n            \"Type\": \"LeastConnection\" // switch from RoundRobin to LeastConnection\r\n          },\r\n          \"RateLimitOptions\": {\r\n            \"EnableRateLimiting\": false // notification service is unlimited!\r\n          }\r\n        }\r\n      ],\r\n      \"GlobalConfiguration\": {\r\n        \"BaseUrl\": \"https://api.ocelot.net\",\r\n        \"DownstreamScheme\": \"http\",\r\n        \"ServiceDiscoveryProvider\": {\r\n          \"Host\": \"localhost\",\r\n          \"Port\": 8500,\r\n          \"Type\": \"Consul\",\r\n          \"Namespace\": \"\" // not supported for Consul, but supported for Kubernetes\r\n        },\r\n        \"CacheOptions\": {\r\n          \"TtlSeconds\": 300 // 5 minutes\r\n        },\r\n        \"HttpHandlerOptions\": {\r\n          \"PooledConnectionLifetimeSeconds\": 600, // change the default value from 2 minutes to 10 minutes\r\n          \"UseTracing\": true // enable tracing globally\r\n        },\r\n        \"LoadBalancerOptions\": {\r\n          \"Type\": \"RoundRobin\"\r\n        },\r\n        \"RateLimitOptions\": {\r\n          \"ClientIdHeader\": \"Oc-DynamicRouting-Client\",\r\n          \"ClientWhitelist\": [\"ocelot-client1-preshared-key\"],\r\n          \"Limit\": 5,\r\n          \"Period\": \"10s\", // fixed window\r\n          \"QuotaMessage\": \"No Quota!\",\r\n          \"StatusCode\": 499 // special shared status\r\n        }\r\n      }\r\n    }\r\n\r\nThis configuration means that when a request is sent to Ocelot at ``/product/*``, *dynamic routing* is activated, and Ocelot applies the rate limiting rules defined for the '``product``' service in the ``DynamicRoutes`` section, as described in the :doc:`../features/ratelimiting` documentation.\r\nThe '``notification``' service is unlimited because both caching, tracing, and rate limiting are disabled.\r\nAll other services use the global ``RateLimitOptions`` along with the other specified options.\r\n\r\n.. warning::\r\n  Dynamic route ``RateLimitRule`` option is deprecated!\r\n\r\n  The `old schema <https://github.com/ThreeMammals/Ocelot/blob/24.0.0/src/Ocelot/Configuration/File/FileDynamicRoute.cs>`_ ``RateLimitRule`` section is deprecated in version `24.1`_!\r\n  Use ``RateLimitOptions`` instead of ``RateLimitRule``! Note that ``RateLimitRule`` will be removed in version `25.0`_!\r\n  For backward compatibility in version `24.1`_, the ``RateLimitRule`` section takes precedence over the ``RateLimitOptions`` section.\r\n\r\n.. note::\r\n\r\n  The ``ServiceNamespace`` option was introduced in version `24.1`_ to enable precise overrides for the :doc:`../features/kubernetes` providers.\r\n  If ``ServiceNamespace`` is left empty or undefined, only **one** dynamic route with the same ``ServiceName`` may be defined in the ``DynamicRoutes`` collection.\r\n\r\n.. _sd-custom-providers:\r\n\r\nCustom Providers\r\n----------------\r\n\r\nOcelot also enables you to create a custom *Service Discovery* implementation by implementing the ``IServiceDiscoveryProvider`` interface, as demonstrated in the following example:\r\n\r\n.. code-block:: csharp\r\n\r\n  public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider\r\n  {\r\n      private readonly IServiceProvider _serviceProvider;\r\n      private readonly ServiceProviderConfiguration _config;\r\n      private readonly DownstreamRoute _downstreamRoute;\r\n\r\n      public MyServiceDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute)\r\n      {\r\n          _serviceProvider = serviceProvider;\r\n          _config = config;\r\n          _downstreamRoute = downstreamRoute;\r\n      }\r\n\r\n      public Task<List<Service>> GetAsync()\r\n      {\r\n          var services = new List<Service>();\r\n          // ...\r\n          // Add service(s) to the list matching the _downstreamRoute\r\n          return services;\r\n      }\r\n  }\r\n\r\nAnd set its class name as the provider type in `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n\r\n  \"GlobalConfiguration\": {\r\n    \"ServiceDiscoveryProvider\": {\r\n      \"Type\": \"MyServiceDiscoveryProvider\"\r\n    }\r\n  }\r\n  \r\nFinally, in the `Program`_, register a ``ServiceDiscoveryFinderDelegate`` to initialize and return the provider:\r\n\r\n.. code-block:: csharp\r\n\r\n  ServiceDiscoveryFinderDelegate serviceDiscoveryFinder = (provider, config, route)\r\n      => new MyServiceDiscoveryProvider(provider, config, route);\r\n  builder.Services\r\n      .AddSingleton(serviceDiscoveryFinder)\r\n      .AddOcelot(builder.Configuration);\r\n\r\n.. _sd-sample:\r\n\r\nSample\r\n------\r\n\r\nTo offer a basic template for a :ref:`sd-custom-providers`, we have created a sample:\r\n\r\n  | Project: `samples <https://github.com/ThreeMammals/Ocelot/tree/main/samples>`_ / `ServiceDiscovery <https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery>`_\r\n  | Solution: `Ocelot.Samples.ServiceDiscovery.sln <https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln>`_\r\n\r\nThis solution includes the following projects:\r\n\r\n- :ref:`sd-api-gateway`\r\n- :ref:`sd-downstream-service`\r\n\r\nThe solution is ready for deployment. All services are fully configured, with ports and hosts prepared for immediate use (when running in Visual Studio).\r\nComplete instructions for running this solution can be found in the `README.md <https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/README.md>`_ file.\r\n\r\n.. _sd-downstream-service:\r\n\r\nDownstreamService\r\n^^^^^^^^^^^^^^^^^\r\n\r\nThis project provides a single downstream service that can be reused across :ref:`sd-api-gateway` routes.\r\nIt includes multiple ``launchSettings.json`` profiles to support your preferred launch and hosting scenarios, such as Visual Studio sessions, Kestrel console hosting, and Docker deployments.\r\n\r\n.. _sd-api-gateway:\r\n\r\nApiGateway\r\n^^^^^^^^^^\r\n\r\nThis project includes a custom *Service Discovery* provider and contains only route(s) to :ref:`sd-downstream-service` services in the `ocelot.json`_ file.\r\nYou are free to add more routes!\r\n\r\nThe main source code for the custom provider is located in the `ServiceDiscovery <https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery>`__ folder, specifically in the ``MyServiceDiscoveryProvider`` and ``MyServiceDiscoveryProviderFactory`` classes.\r\nFeel free to design and develop these classes to suit your needs!\r\n\r\nAdditionally, the cornerstone of this custom provider is the `Program`_ code, where you can select from simple or more complex design and implementation options:\r\n\r\n  .. code-block:: csharp\r\n\r\n    // Perform initialization from application configuration or hardcode/choose the best option.\r\n    bool easyWay = true;\r\n    if (easyWay)\r\n    {\r\n        // Design #1: Define a custom finder delegate to instantiate a custom provider \r\n        // under the default factory (ServiceDiscoveryProviderFactory).\r\n        builder.Services\r\n            .AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)\r\n                => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute));\r\n    }\r\n    else\r\n    {\r\n        // Design #2: Abstract from the default factory (ServiceDiscoveryProviderFactory) and FinderDelegate,\r\n        // and create your own factory by implementing the IServiceDiscoveryProviderFactory interface.\r\n        builder.Services\r\n            .RemoveAll<IServiceDiscoveryProviderFactory>()\r\n            .AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();\r\n\r\n        // This will not be called but is required for internal validators. It's also a handy workaround.\r\n        builder.Services\r\n            .AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute) => null);\r\n    }\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\nThe \"easy way\" (lite design #1) involves designing only the provider class and specifying the ``ServiceDiscoveryFinderDelegate`` object for the default ``ServiceDiscoveryProviderFactory`` in the Ocelot core.\r\n\r\nA more complex design #2 involves developing both the provider and provider factory classes.\r\nOnce this is done, you need to add the ``IServiceDiscoveryProviderFactory`` interface to the DI container and remove the default ``ServiceDiscoveryProviderFactory`` class.\r\nNote that in this case, the Ocelot core will not use the ``ServiceDiscoveryProviderFactory`` by default.\r\nAdditionally, you do not need to specify ``\"Type\": \"MyServiceDiscoveryProvider\"`` in the ``ServiceDiscoveryProvider`` global options.\r\nHowever, you can retain this ``Type`` option to maintain compatibility between both designs.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\r\n.. [#f2] The \":ref:`Configuration Key <sd-consul-configuration-key>`\" feature was requested in issue `346`_ and introduced in version `7.0.0`_.\r\n.. [#f3] The customization of \":ref:`Consul Service Builder <sd-consul-service-builder>`\" was implemented as part of bug fix `954`_, and the feature was delivered in version `23.3`_.\r\n.. [#f4] The :ref:`Eureka <sd-eureka>` feature, requested in issue `262`_ to add support for the Netflix `Eureka`_ *service discovery* provider, was released in version `5.5.4`_.\r\n.. [#f5] The \":ref:`Dynamic Routing <sd-dynamic-routing>`\" feature was requested in issue `340`_ (pull request `351`_) and released in version `7.0.1`_.\r\n  Later, the new ``DynamicRoutes`` :doc:`../features/configuration` section was introduced in pull request `508`_ and released in version `8.0.4`_.\r\n.. [#f6] The :ref:`Configuration <sd-dynamic-routing-configuration>` feature of :ref:`Dynamic Routing <sd-dynamic-routing>` was requested in issue `585`_, then significantly redeveloped and released in version `24.1`_.\r\n\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/ApiGateway/ocelot.json\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/ApiGateway/Program.cs\r\n.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv\r\n.. _3-second TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code\r\n.. _catalog nodes: https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes\r\n.. _acceptance test: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+ShouldReturnServiceAddressByOverriddenServiceBuilderWhenThereIsANode+WithConsulServiceBuilder&type=code\r\n\r\n.. _262: https://github.com/ThreeMammals/Ocelot/issues/262\r\n.. _340: https://github.com/ThreeMammals/Ocelot/issues/340\r\n.. _346: https://github.com/ThreeMammals/Ocelot/issues/346\r\n.. _351: https://github.com/ThreeMammals/Ocelot/pull/351\r\n.. _508: https://github.com/ThreeMammals/Ocelot/pull/508\r\n.. _585: https://github.com/ThreeMammals/Ocelot/issues/585\r\n.. _909: https://github.com/ThreeMammals/Ocelot/pull/909\r\n.. _954: https://github.com/ThreeMammals/Ocelot/issues/954\r\n.. _1154: https://github.com/ThreeMammals/Ocelot/pull/1154\r\n\r\n.. _5.5.4: https://github.com/ThreeMammals/Ocelot/releases/tag/5.5.4\r\n.. _7.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.0\r\n.. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1\r\n.. _8.0.4: https://github.com/ThreeMammals/Ocelot/releases/tag/8.0.4\r\n.. _13.5.2: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.2\r\n.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n.. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13\r\n"
  },
  {
    "path": "docs/features/servicefabric.rst",
    "content": "Service Fabric\r\n==============\r\n\r\n  [#f1]_ Feature of: :doc:`../features/servicediscovery`\r\n\r\nIf you have services deployed in Azure `Service Fabric`_ you will normally use the naming service to access them.\r\n\r\nThis feature allows to set up a route that will work in `Service Fabric`_.\r\n\r\nConfiguration\r\n-------------\r\n\r\nThe most important thing is the ``ServiceName``, which is composed of the `Service Fabric`_ application name followed by the specific service name.\r\nAdditionally, the ``ServiceDiscoveryProvider`` needs to be configured in ``GlobalConfiguration``.\r\nThe example below demonstrates a typical configuration.\r\nIt assumes that *Service Fabric* is running on ``localhost`` and that the naming service is using port ``19081``.\r\n\r\n  The example below is taken from the :ref:`sf-sample`, so please check it if this doesn't make sense!\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 8, 17\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        \"DownstreamScheme\": \"http\",\r\n        \"DownstreamPathTemplate\": \"/api/values\",\r\n        \"UpstreamPathTemplate\": \"/EquipmentInterfaces\",\r\n        \"UpstreamHttpMethod\": [ \"Get\" ],\r\n        \"ServiceName\": \"OcelotServiceApplication/OcelotApplicationService\"\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://ocelot.net\",\r\n      \"RequestIdKey\": \"Oc-RequestId\",\r\n      \"ServiceDiscoveryProvider\": {\r\n        \"Host\": \"localhost\",\r\n        \"Port\": 19081,\r\n        \"Type\": \"ServiceFabric\"\r\n      }\r\n    }\r\n  }\r\n\r\nIf you are using stateless or guest exe services, Ocelot can proxy through the naming service without requiring additional configuration.\r\nHowever, if you are using stateful or actor services, you must include the ``PartitionKind`` and ``PartitionKey`` query string values in the client request, e.g.,\r\n\r\n  GET ``http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx``\r\n\r\nThere is no way for Ocelot to determine these values automatically.\r\n\r\n.. _sf-placeholders:\r\n\r\nPlaceholders [#f2]_\r\n-------------------\r\n\r\nIn Ocelot, *placeholders* for variables can be inserted into the ``UpstreamPathTemplate`` and ``ServiceName`` using the format ``{something}``.\r\n\r\n  **Note**: The *placeholder* variable must exist in both the ``DownstreamPathTemplate`` (or ``ServiceName``) and the ``UpstreamPathTemplate``.\r\n  Specifically, the ``UpstreamPathTemplate`` must include all *placeholders* found in the ``DownstreamPathTemplate`` and ``ServiceName``.\r\n  Failure to meet this requirement will prevent Ocelot from starting due to validation errors, which are logged.\r\n\r\nOnce the validation stage is completed, Ocelot replaces the placeholder values in the ``UpstreamPathTemplate`` with those from the ``DownstreamPathTemplate`` and/or ``ServiceName`` for each processed request.\r\nThus, the *Service Fabric* :ref:`sf-placeholders` feature operates similarly to the original routing :ref:`routing-placeholders` feature but includes the ``ServiceName`` property in its processing.\r\n\r\n| Here is an example of the ``version`` variable in the *Service Fabric* service name.\r\n| Given the following `ocelot.json`_:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 6, 14\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        \"UpstreamPathTemplate\": \"/api/{version}/{endpoint}\",\r\n        \"DownstreamPathTemplate\": \"/{endpoint}\",\r\n        \"ServiceName\": \"Service_{version}/Api\",\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://ocelot.com\",\r\n      \"ServiceDiscoveryProvider\": {\r\n        \"Host\": \"localhost\",\r\n        \"Port\": 19081,\r\n        \"Type\": \"ServiceFabric\"\r\n      }\r\n    }\r\n  }\r\n\r\nWhen you make Ocelot request:\r\n\r\n* ``GET https://ocelot.com/api/1.0/products``\r\n\r\nThe *Service Fabric* request will be:\r\n\r\n* ``GET http://localhost:19081/Service_1.0/Api/products``\r\n\r\n.. _sf-sample:\r\n\r\nSample\r\n------\r\n\r\nIn order to introduce the *Service Fabric* feature, we have prepared a sample:\r\n\r\n  | Project: `samples <https://github.com/ThreeMammals/Ocelot/tree/main/samples>`_ / `ServiceFabric <https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric/>`_\r\n  | Solution: `Ocelot.Samples.ServiceFabric.sln <https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric/Ocelot.Samples.ServiceFabric.sln>`_\r\n\r\nThis solution includes the following projects:\r\n\r\n- ``Ocelot.Samples.ServiceFabric.ApiGateway.csproj``\r\n- ``Ocelot.Samples.ServiceFabric.DownstreamService.csproj``\r\n\r\nComplete instructions for running this solution can be found in the `README.md <https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceFabric/README.md>`_ file.\r\n\r\n.. note::\r\n  Please consider this solution as a demonstration of integration; it is outdated as of 2025.\r\n  Therefore, this solution is a draft and requires further development for practical usage and deployment in the Azure cloud.\r\n  Additionally, refer to the team's notes in the :ref:`sd-service-fabric` section!\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] Historically, the \"`Service Fabric <#service-fabric>`__\" feature is one of Ocelot's earliest and foundational features, first requested in issue `238`_. It was initially released in version `3.1.9`_.\r\n.. [#f2] The \":ref:`Placeholders <sf-placeholders>`\" feature was requested in issue `721`_ and implemented by pull request `722`_ as part of version `13.0.0`_.\r\n\r\n.. _Service Fabric: https://azure.microsoft.com/en-us/products/service-fabric/\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceFabric/ApiGateway/ocelot.json\r\n\r\n.. _238: https://github.com/ThreeMammals/Ocelot/issues/238\r\n.. _721: https://github.com/ThreeMammals/Ocelot/issues/721\r\n.. _722: https://github.com/ThreeMammals/Ocelot/pull/722\r\n.. _3.1.9: https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.9\r\n.. _13.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.0.0\r\n"
  },
  {
    "path": "docs/features/tracing.rst",
    "content": "Tracing\r\n=======\r\n\r\n  Feature of: :doc:`../features/logging`\r\n\r\n  * `.NET logging and tracing | .NET | Microsoft Learn <https://learn.microsoft.com/en-us/dotnet/core/diagnostics/logging-tracing>`_\r\n  * `.NET distributed tracing | .NET | Microsoft Learn <https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing>`_\r\n\r\nThis chapter explains how to perform distributed tracing using Ocelot.\r\n\r\n.. |opentracing-csharp Logo| image:: https://avatars.githubusercontent.com/u/15482765\r\n  :alt: opentracing-csharp Logo\r\n  :width: 30\r\n\r\n|opentracing-csharp Logo| OpenTracing\r\n-------------------------------------\r\n\r\n.. _OpenTracing: https://opentracing.io\r\n\r\n  | Package: `Ocelot.Tracing.OpenTracing <https://www.nuget.org/packages/Ocelot.Tracing.OpenTracing>`_\r\n  | Namespace: ``Ocelot.Tracing.OpenTracing``\r\n\r\nOcelot provides tracing functionality through the excellent project from `opentracing-csharp <https://github.com/opentracing/opentracing-csharp>`_ repository.\r\nThe code for Ocelot integration can be found in this `Ocelot project <https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Tracing.OpenTracing>`_.\r\n\r\nThe example below uses the `C# Client for Jaeger <https://github.com/jaegertracing/jaeger-client-csharp>`_ to provide the tracer used in Ocelot.\r\nTo add `OpenTracing`_ services, you must call the ``AddOpenTracing()`` extension method on the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_, as shown below:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 11\r\n\r\n  builder.Services\r\n      .AddSingleton(serviceProvider =>\r\n      {\r\n          var loggerFactory = serviceProvider.GetService<ILoggerFactory>();\r\n          var config = new Jaeger.Configuration(builder.Environment.ApplicationName, loggerFactory);\r\n          var tracer = config.GetTracer();\r\n          GlobalTracer.Register(tracer);\r\n          return tracer;\r\n      })\r\n      .AddOcelot(builder.Configuration)\r\n      .AddOpenTracing();\r\n\r\nThen, in your `ocelot.json <https://github.com/ThreeMammals/Ocelot/blob/main/samples/OpenTracing/ocelot.json>`__, add the following to the route you want to trace:\r\n\r\n.. code-block:: json\r\n\r\n  \"HttpHandlerOptions\": {\r\n    \"UseTracing\": true\r\n  }\r\n\r\nOcelot will now send tracing information to `Jaeger <https://www.jaegertracing.io/>`_ whenever this route is called.\r\n\r\n  **Note 1**: A clean yet functional sample can be found here: `Ocelot.Samples.OpenTracing <https://github.com/ThreeMammals/Ocelot/tree/main/samples/OpenTracing>`_.\r\n\r\n  **Note 2**: The `OpenTracing`_ project was archived on January 31, 2022 (see `the article <https://www.cncf.io/blog/2022/01/31/cncf-archives-the-opentracing-project/>`_).\r\n  The Ocelot team is planning to decide on a migration to `OpenTelemetry <https://opentelemetry.io>`_, which is highly desirable.\r\n\r\n.. _tr-butterfly:\r\n\r\nButterfly\r\n---------\r\n\r\n.. _Butterfly: https://github.com/liuhaoyang/butterfly\r\n\r\n  | Package: `Ocelot.Tracing.Butterfly <https://www.nuget.org/packages/Ocelot.Tracing.Butterfly>`_\r\n  | Namespace: ``Ocelot.Tracing.Butterfly``\r\n\r\nOcelot provides tracing functionality through the excellent `Butterfly`_ project.\r\nThe code for the Ocelot integration can be found in this `Ocelot project <https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Tracing.Butterfly>`__.\r\nTo use the tracing functionality, please refer to the `Butterfly`_ documentation.\r\n\r\nIn Ocelot, you need to add the NuGet package if you wish to trace a route:\r\n\r\n.. code-block:: powershell\r\n\r\n    Install-Package Ocelot.Tracing.Butterfly\r\n\r\nIn your `Program`_, to add `Butterfly`_ services, you must call the ``AddButterfly()`` extension method on the ``OcelotBuilder`` returned by ``AddOcelot()``, as shown below:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 5\r\n\r\n  using Ocelot.Tracing.Butterfly;\r\n\r\n  builder.Services\r\n      .AddOcelot(builder.Configuration)\r\n      .AddButterfly(options =>\r\n      {\r\n          // This is the URL that the Butterfly collector server is running on...\r\n          options.CollectorUrl = \"http://localhost:9618\";\r\n          options.Service = \"Ocelot\";\r\n      });\r\n\r\nThen, in your `ocelot.json`_, add the following to the route you want to trace:\r\n\r\n.. code-block:: json\r\n\r\n  \"HttpHandlerOptions\": {\r\n    \"UseTracing\": true\r\n  }\r\n\r\nOcelot will now send tracing information to `Butterfly`_ whenever this route is called.\r\n\r\n  **Note**: The `Butterfly`_ project has not been supported for more than seven years, as of 2025.\r\n  The latest release of the `Butterfly.Client <https://www.nuget.org/packages/Butterfly.Client>`_ package (version `0.0.8 <https://www.nuget.org/packages/Butterfly.Client/0.0.8>`_) was made on February 22, 2018.\r\n  The Ocelot team is planning to discontinue the `Ocelot.Tracing.Butterfly`_ package, which is scheduled to happen after the release of Ocelot version `24.1`_.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\r\n\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\r\n"
  },
  {
    "path": "docs/features/websockets.rst",
    "content": "Websockets\r\n==========\r\n\r\n    * `WebSockets Standard <https://websockets.spec.whatwg.org/>`_ by WHATWG organization\r\n    * `The WebSocket Protocol <https://datatracker.ietf.org/doc/html/rfc6455>`_ by Internet Engineering Task Force (IETF) organization\r\n\r\nOcelot supports proxying `WebSockets <https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API>`_ [#f1]_ with some extra bits.\r\n\r\nConfiguration\r\n-------------\r\n\r\nTo enable *WebSockets* proxying with Ocelot, you need to do the following in your `Program`_:\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 2\r\n\r\n  var app = builder.Build();\r\n  app.UseWebSockets();\r\n  await app.UseOcelot();\r\n  await app.RunAsync();\r\n\r\nThen, in your `ocelot.json`_, add the following to proxy a route using *WebSockets*:\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 4\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/\",\r\n    \"DownstreamPathTemplate\": \"/ws\",\r\n    \"DownstreamScheme\": \"ws\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"localhost\", \"Port\": 5001 }\r\n    ]\r\n  }\r\n\r\nWith this configuration, Ocelot will match any *WebSockets* traffic that comes in on / and proxy it to ``localhost:5001/ws``.\r\nFor clarity, Ocelot will receive messages from the upstream client, proxy them to the downstream service, receive messages from the downstream service, and then proxy them back to the upstream client.\r\n\r\nHandy Links\r\n-----------\r\n\r\n* WHATWG: `WebSockets Standard <https://websockets.spec.whatwg.org/>`_\r\n* Mozilla Developer Network: `The WebSocket API (WebSockets) <https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API>`_\r\n* Microsoft Learn: `WebSockets support in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets>`_\r\n* Microsoft Learn: `WebSockets support in .NET <https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets>`_\r\n\r\n.. _ws-signalr:\r\n\r\nSignalR [#f2]_\r\n--------------\r\n\r\n  Welcome to `Real-time ASP.NET with SignalR <https://dotnet.microsoft.com/en-us/apps/aspnet/signalr>`_\r\n\r\nOcelot supports proxying *SignalR*. To enable this with Ocelot, you need to do the following:\r\n\r\nFirst, install the `SignalR Client <https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client>`_ NuGet package:\r\n\r\n.. code-block:: powershell\r\n\r\n  Install-Package Microsoft.AspNetCore.SignalR.Client\r\n\r\n.. _break: http://break.do\r\n\r\n  **Note**: SignalR is `part of the ASP.NET Core <https://github.com/dotnet/aspnetcore/tree/main/src/SignalR>`_ and can be referenced as follows:\r\n\r\n  .. code-block:: xml\r\n\r\n    <ItemGroup>\r\n      <FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\r\n    </ItemGroup>\r\n\r\n  More information on framework compatibility can be found in the instructions: `Use ASP.NET Core APIs in a class library <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/target-aspnetcore>`_.\r\n\r\nSecond, you need to configure your application to use *SignalR*.\r\nA complete reference can be found here: `ASP.NET Core SignalR configuration <https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration>`_.\r\n\r\n.. code-block:: csharp\r\n\r\n  builder.Services.AddOcelot(builder.Configuration);\r\n  builder.Services.AddSignalR();\r\n\r\n.. _break2: http://break.do\r\n\r\n  **Note**: Make sure to pay attention to the transport-level configuration for *WebSockets*.\r\n  Ensure that allowed transports are properly configured to enable *WebSockets* connections: `ASP.NET Core SignalR configuration <https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration>`_.\r\n\r\nNext, include the following in your `ocelot.json`_ file to proxy a route using *SignalR*.\r\nNote that standard Ocelot routing rules apply; the key aspect is that the scheme is set to ``ws`` (*WebSockets*).\r\n\r\n.. code-block:: json\r\n  :emphasize-lines: 4\r\n\r\n  {\r\n    \"UpstreamPathTemplate\": \"/gateway/{catchAll}\",\r\n    \"DownstreamPathTemplate\": \"/{catchAll}\",\r\n    \"DownstreamScheme\": \"ws\",\r\n    \"DownstreamHostAndPorts\": [\r\n      { \"Host\": \"localhost\", \"Port\": 5001 }\r\n    ]\r\n  }\r\n\r\n.. _ws-secure:\r\n\r\nWebSocket Secure\r\n----------------\r\n\r\nIf you define a route with the *secured WebSockets* protocol, use the ``wss`` scheme:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamScheme\": \"wss\",\r\n\r\nKeep in mind that you can use WebSocket SSL for both :ref:`SignalR <ws-signalr>` and :doc:`../features/websockets`.\r\n\r\n  **Note**: To understand ``wss`` scheme, browse to this documentation:\r\n\r\n  * IETF | The WebSocket Protocol: `WebSocket URIs <https://datatracker.ietf.org/doc/html/rfc6455#section-3>`_\r\n  * Microsoft Learn: `Secure your connection with TLS/SSL <https://learn.microsoft.com/en-us/windows/uwp/networking/websockets#secure-your-connection-with-tlsssl>`_\r\n  * Microsoft Learn: `Search for \"secure websocket\" <https://learn.microsoft.com/en-us/search/?terms=secure%20websocket>`_\r\n\r\nIf you want to ignore SSL warnings (errors) [#f3]_, configure your route as follows:\r\n\r\n.. code-block:: json\r\n\r\n  \"DownstreamScheme\": \"wss\",\r\n  \"DangerousAcceptAnyServerCertificateValidator\": true,\r\n\r\n*However, we strongly advise against this!*\r\nRefer to the official notes regarding :ref:`ssl-errors` in the :doc:`../features/configuration` documentation.\r\nThere, you can also explore best practices tailored for your environments.\r\n\r\nSupported\r\n---------\r\n\r\n1. :doc:`../features/routing`\r\n2. :doc:`../features/loadbalancer`\r\n3. :doc:`../features/servicediscovery`\r\n\r\nThis means you can configure your downstream services to run *WebSockets* and either:\r\n\r\n* Include multiple ``DownstreamHostAndPorts`` in your route configuration.\r\n* Connect your route to a :doc:`../features/servicediscovery` provider.\r\n  This allows you to load balance requests, which we think is pretty cool!\r\n\r\nNot Supported\r\n-------------\r\n\r\nUnfortunately, many Ocelot features are not specific to *WebSockets*, such as header handling and HTTP client functionalities.\r\nBelow is a list of features that will not work:\r\n\r\n1. :doc:`../features/tracing`\r\n2. :doc:`../features/logging` :ref:`lg-request-id`\r\n3. :doc:`../features/aggregation`\r\n4. :doc:`../features/ratelimiting`\r\n5. :doc:`../features/qualityofservice`\r\n6. :doc:`../features/middlewareinjection`\r\n7. :doc:`../features/headerstransformation`\r\n8. :doc:`../features/delegatinghandlers`\r\n9. :doc:`../features/claimstransformation`\r\n10. :doc:`../features/caching`\r\n11. :doc:`../features/authentication` [#f4]_\r\n12. :doc:`../features/authorization`\r\n\r\nWe cannot be entirely sure how this feature will behave once it is widely used. Therefore, thorough testing is strongly recommended!\r\n\r\nRoadmap\r\n-------\r\n\r\n*WebSockets* and *SignalR* are being actively developed by the .NET community.\r\nIt is important to stay updated with trends and regularly check for new releases in the official documentation:\r\n\r\n* `WebSockets docs <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets>`_\r\n* `SignalR docs <https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction>`_\r\n\r\nAs a team, we are unable to provide direct development advice.\r\nHowever, feel free to ask questions or explore coding recipes in `Discussions <https://github.com/ThreeMammals/Ocelot/discussions>`_ of the repository.\r\nAdditionally, we welcome any bug reports, enhancement suggestions, or proposals related to this feature. |octocat|\r\n\r\n.. note::\r\n  The Ocelot team considers the current implementation of the *WebSockets* feature to be obsolete, as it is based on the `WebSocketsProxyMiddleware <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20WebSocketsProxyMiddleware&type=code>`_ class.\r\n  *WebSockets* are a part of the ASP.NET Core framework, which includes the native `WebSocketMiddleware <https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.websockets.websocketmiddleware>`_ class.\r\n  We have a strong intention to either migrate or redesign this feature. For more details, see issue `1707`_.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The :doc:`../features/websockets` functionality was requested in issue `212 <https://github.com/ThreeMammals/Ocelot/issues/212>`_ and introduced in version `5.3.0`_.\r\n.. [#f2] The :ref:`SignalR <ws-signalr>` functionality was requested in issue `344`_ and published in version `8.0.7`_.\r\n.. [#f3] The \":ref:`ws-secure`\"  feature includes a ``wss`` scheme fake validator, which was introduced in pull request `1377`_ as part of issues `1375`_, `1237`_, and others.\r\n  This \"life hack\" for self-signed SSL certificates is available starting from version `20.0`_.\r\n  However, it will be either removed or reworked in future releases. For further details, refer to the :ref:`ssl-errors` section.\r\n.. [#f4] If requested, we might explore options for implementing basic authentication.\r\n\r\n.. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n\r\n.. _212: https://github.com/ThreeMammals/Ocelot/issues/212\r\n.. _344: https://github.com/ThreeMammals/Ocelot/issues/344\r\n.. _1237: https://github.com/ThreeMammals/Ocelot/issues/1237\r\n.. _1375: https://github.com/ThreeMammals/Ocelot/issues/1375\r\n.. _1377: https://github.com/ThreeMammals/Ocelot/pull/1377\r\n.. _1707: https://github.com/ThreeMammals/Ocelot/issues/1707\r\n.. _5.3.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.3.0\r\n.. _8.0.7: https://github.com/ThreeMammals/Ocelot/releases/tag/8.0.7\r\n.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0\r\n\r\n.. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png\r\n  :alt: octocat\r\n  :height: 25\r\n  :class: img-valign-middle\r\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0\r\n.. role::  htm(raw)\r\n    :format: html\r\n.. role:: pdf(raw)\r\n   :format: latex pdflatex\r\n\r\n##############\r\nOcelot `25.0`_\r\n##############\r\n\r\nThanks for taking a look at the Ocelot documentation!\r\nPlease use the left hand **Navigation** sidebar to get around, or see the :htm:`<a class=\"reference internal\" href=\"#toc\"><span class=\"std std-ref\">Table of Contents</span></a> below.` :pdf:`\\textbf{Table of Contents} above.`\r\n\r\nThe team recommends that newcomers to Ocelot's world start with the \":doc:`Introduction <../introduction/bigpicture>`\" chapters.\r\nFor seasoned fans of Ocelot with a Production environment, it is advised to always consult the :ref:`release-notes` in the :doc:`../releasenotes` section before upgrading the app to the latest `25.0`_ version.\r\n\r\nAll **Features** are listed in alphabetical order.\r\nThe primary features include :doc:`../features/configuration` and :doc:`../features/routing`.\r\n\r\nAdditional tips for building Ocelot can be found in the \":doc:`Building Ocelot <../building/building>`\" section.\r\nWe adhere to a :doc:`../building/devprocess` which is a part of :doc:`../building/releaseprocess`.\r\n\r\n:htm:`<section id=\"toc\"><h2>Table of Contents<a class=\"headerlink\" href=\"#toc\" title=\"Link to this heading\">¶</a></h2>`\r\n\r\n  .. toctree::\r\n    :maxdepth: 2\r\n    :caption: Welcome\r\n\r\n    releasenotes\r\n\r\n  .. toctree::\r\n    :maxdepth: 3\r\n    :caption: Introduction\r\n\r\n    introduction/bigpicture\r\n    introduction/gettingstarted\r\n    introduction/notsupported\r\n    introduction/gotchas\r\n\r\n  .. toctree::\r\n    :maxdepth: 3\r\n    :caption: Features\r\n\r\n    features/administration\r\n    features/aggregation\r\n    features/authentication\r\n    features/authorization\r\n    features/caching\r\n    features/claimstransformation\r\n    features/configuration\r\n    features/delegatinghandlers\r\n    features/dependencyinjection\r\n    features/errorcodes\r\n    features/graphql\r\n    features/headerstransformation\r\n    features/kubernetes\r\n    features/loadbalancer\r\n    features/logging\r\n    features/metadata\r\n    features/methodtransformation\r\n    features/middlewareinjection\r\n    features/qualityofservice\r\n    features/ratelimiting\r\n    features/routing\r\n    features/servicediscovery\r\n    features/servicefabric\r\n    features/tracing\r\n    features/websockets\r\n\r\n  .. toctree::\r\n    :maxdepth: 3\r\n    :caption: Building Ocelot\r\n\r\n    building/building\r\n    building/devprocess\r\n    building/releaseprocess\r\n\r\n:htm:`</section>`\r\n"
  },
  {
    "path": "docs/introduction/bigpicture.rst",
    "content": "Big Picture\r\n===========\r\n\r\nOcelot is aimed at people using .NET running a microservices (service-oriented) architecture (aka  SOA) that need a unified point of entry into their system.\r\nHowever it will work with anything that speaks HTTP(S) and run on any platform that `ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/>`_ supports.\r\n\r\n..\r\n    TODO Decide what to do with this paragraph?..\r\n    In particular we want easy integration with `IdentityServer <https://github.com/IdentityServer>`_ reference and `Bearer <https://oauth.net/2/bearer-tokens/>`_ tokens. \r\n    We have been unable to find this in our current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens.\r\n    We would rather use the IdentityServer code that already exists to do this.\r\n\r\nOcelot consists of a series of ASP.NET Core `middlewares <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/>`_ arranged in a specific order.\r\n\r\nOcelot manipulates the ``HttpRequest`` object into a state specified by its configuration until it reaches a request builder middleware,\r\nwhere it creates a ``HttpRequestMessage`` object which is used to make a request to a downstream service.\r\nThe middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware.\r\nThe response from the downstream service is retrieved as the request goes back up the Ocelot pipeline.\r\nThere is a piece of middleware that maps the ``HttpResponseMessage`` onto the ``HttpResponse`` object, and that is returned to the client.\r\nThat is basically it with a bunch of other features!\r\n\r\nThe following are configurations that you use when deploying Ocelot.\r\n\r\nBasic Implementation\r\n--------------------\r\n.. image:: ../images/OcelotBasic.jpg\r\n\r\n..\r\n    TODO Do not advertise the product because of non-OSS status\r\n    With IdentityServer\r\n    ^^^^^^^^^^^^^^^^^^^\r\n    .. image:: ../images/OcelotIndentityServer.jpg\r\n\r\nMultiple Instances\r\n------------------\r\n.. image:: ../images/OcelotMultipleInstances.jpg\r\n\r\nWith Consul\r\n-----------\r\n.. image:: ../images/OcelotMultipleInstancesConsul.jpg\r\n\r\nWith Service Fabric\r\n-------------------\r\n.. image:: ../images/OcelotServiceFabric.jpg\r\n"
  },
  {
    "path": "docs/introduction/gettingstarted.rst",
    "content": "Getting Started\r\n===============\r\n\r\nOcelot is designed to work with `ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-9.0>`_ and is currently on `.NET 8 <https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#lifecycle>`_ `LTS <https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types>`_\r\nand `.NET 9 <https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#lifecycle>`_ `STS <https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types>`_ frameworks.\r\n\r\nInstall\r\n-------\r\n\r\nInstall Ocelot and it's dependencies using `NuGet <https://www.nuget.org/>`_.\r\nYou will need to create a `ASP.NET Core minimal API project <https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api>`_ with \"ASP.NET Core Empty\" template but without ``app.Map*`` methods, and bring the package into it.\r\nThen follow the startup below and :doc:`../features/configuration` sections to get up and running.\r\n\r\n.. code-block:: powershell\r\n\r\n   Install-Package Ocelot\r\n\r\nAll versions can be found in the `NuGet Gallery | Ocelot <https://www.nuget.org/packages/Ocelot/>`_.\r\n\r\n.. _getstarted-configuration:\r\n\r\nConfiguration\r\n-------------\r\n\r\nThe following is a very basic `ocelot.json`_.\r\nIt won't do anything but should get Ocelot starting.\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Aggregates\": [], // optional\r\n    \"Routes\": [], // required section\r\n    \"DynamicRoutes\": [], // optional section\r\n    \"GlobalConfiguration\": { // required\r\n      \"BaseUrl\": \"https://api.mybusiness.com\"\r\n    }\r\n  }\r\n\r\nIf you want some example that actually does something use the following:\r\n\r\n.. code-block:: json\r\n\r\n  {\r\n    \"Routes\": [\r\n      {\r\n        \"UpstreamHttpMethod\": [ \"Get\" ],\r\n        \"UpstreamPathTemplate\": \"/ocelot/posts/{id}\",\r\n        \"DownstreamPathTemplate\": \"/todos/{id}\",\r\n        \"DownstreamScheme\": \"https\",\r\n        \"DownstreamHostAndPorts\": [\r\n          { \"Host\": \"jsonplaceholder.typicode.com\", \"Port\": 443 }\r\n        ]\r\n      }\r\n    ],\r\n    \"GlobalConfiguration\": {\r\n      \"BaseUrl\": \"https://api.mybusiness.com\"\r\n    }\r\n  }\r\n\r\nThe most important thing to note here is ``BaseUrl`` property.\r\nOcelot needs to know the URL it is running under in order to do Header :ref:`ht-find-and-replace` and for certain :doc:`../features/administration` configurations.\r\nWhen setting this URL it should be the external URL that clients will see Ocelot running on e.g.\r\nIf you are running containers Ocelot might run on the URL ``http://123.12.1.2:6543`` but has something like `nginx <https://nginx.org/>`_ in front of it responding on ``https://api.mybusiness.com``.\r\nIn this case the Ocelot ``BaseUrl`` should be ``https://api.mybusiness.com``. \r\n\r\nIf you are using containers and require Ocelot to respond to clients on ``http://123.12.1.2:6543`` then you can do this,\r\nhowever if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script.\r\nHopefully whatever scheduler you are using can pass the IP.\r\n\r\nProgram\r\n-------\r\n\r\nThen in your `Program.cs <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs>`_ (with `top-level statements <https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements>`_) you will want to have the following.\r\n\r\n.. code-block:: csharp\r\n  :emphasize-lines: 9,11,21\r\n\r\n    using Ocelot.DependencyInjection;\r\n    using Ocelot.Middleware;\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n\r\n    // Ocelot Basic setup\r\n    builder.Configuration\r\n        .SetBasePath(builder.Environment.ContentRootPath)\r\n        .AddOcelot(); // single ocelot.json file in read-only mode\r\n    builder.Services\r\n        .AddOcelot(builder.Configuration);\r\n\r\n    // Add your features\r\n    if (builder.Environment.IsDevelopment())\r\n    {\r\n        builder.Logging.AddConsole();\r\n    }\r\n\r\n    // Add middlewares aka app.Use*()\r\n    var app = builder.Build();\r\n    await app.UseOcelot();\r\n    await app.RunAsync();\r\n\r\nThe main things to note are\r\n\r\n* ``builder.Configuration.AddOcelot()`` adds single `ocelot.json`_ configuration file in read-only mode.\r\n* ``builder.Services.AddOcelot(builder.Configuration)`` adds Ocelot required and default services [#f1]_\r\n* ``app.UseOcelot()`` sets up all the Ocelot middlewares. Note, we have to await the threading result before calling ``app.RunAsync()``\r\n* Do not add endpoint mappings (minimal API methods) such as ``app.MapGet()`` because the Ocelot pipeline is not compatible with them!\r\n\r\n\r\n.. _gettingstarted-samples:\r\n\r\nSamples\r\n-------\r\n\r\n  **Solution**: `Ocelot.Samples.sln`_\r\n\r\nFor beginners, we have prepared basic `samples <https://github.com/ThreeMammals/Ocelot/tree/main/samples>`_ to help Ocelot newbies clone, compile, and get it running.\r\n\r\n* `Basic <https://github.com/ThreeMammals/Ocelot/tree/main/samples/Basic>`_ sample: It has a single configuration file, `ocelot.json`_.\r\n* `Basic Configuration <https://github.com/ThreeMammals/Ocelot/tree/main/samples/Configuration>`_ sample: It has multiple configuration files (``ocelot.*.json``) to be merged into ``ocelot.json`` and written back to disk.\r\n\r\nAfter running in Visual Studio [#f2]_, you may use ``API.http`` files to send testing requests to the ``localhost`` Ocelot application instance.\r\n\r\n\"\"\"\"\r\n\r\n.. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the \":ref:`di-addocelotusingbuilder-method`\" section of the :doc:`../features/dependencyinjection` feature.\r\n.. [#f2] All :ref:`gettingstarted-samples` projects are organized as the `Ocelot.Samples.sln`_ file for Visual Studio 2022 IDE.\r\n\r\n.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json\r\n.. _Ocelot.Samples.sln: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Ocelot.Samples.sln\r\n"
  },
  {
    "path": "docs/introduction/gotchas.rst",
    "content": ".. role:: htm(raw)\n  :format: html\n.. role:: pdf(raw)\n  :format: latex pdflatex\n\nHosting Gotchas\n===============\n\n    Microsoft Learn: `Web server implementations in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/>`_\n\nMany errors and incidents (gotchas) are related to web server hosting scenarios.\nPlease review deployment and web hosting common user scenarios below depending on your web server.\n\n.. _hosting-gotchas-iis:\n\nIIS\n---\n\n  | Repository Label: |image-IIS|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/labels/IIS}{IIS}`\n  | Microsoft Learn: `Host ASP.NET Core on Windows with IIS <https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/>`_\n\nWe **do not** recommend to deploy Ocelot app to IIS environments, but if you do, keep in mind the gotchas below.\n\n* When using ASP.NET Core 2.2+ and you want to use In-Process hosting, replace ``UseIISIntegration()`` with ``UseIIS()``, otherwise you will get startup errors.\n\n* Make sure you use Out-of-process hosting model instead of In-process one\n  (see `Out-of-process hosting with IIS and ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/out-of-process-hosting>`_),\n  otherwise you will get very slow responses (see `1657`_).\n\n* Ensure all DNS servers of all downstream hosts are online and they function perfectly, otherwise you will get slow responses (see `1630`_).\n\nThe community constanly reports `issues related to IIS <https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+IIS>`_.\nIf you have some troubles in IIS environment to host Ocelot app, first of all, read open/closed issues, and after that, search for `IIS-related objects`_ in the repository.\nProbably you will find a ready solution by Ocelot community members. \n\n    Finally, there is the special |image-IIS|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/labels/IIS}{IIS}` label for all `IIS-related objects`_.\n    Feel free to put this label onto `issues <https://github.com/ThreeMammals/Ocelot/labels/IIS>`_, `pull requests <https://github.com/ThreeMammals/Ocelot/pulls?q=is%3Apr+label%3AIIS+>`_, `discussions <https://github.com/ThreeMammals/Ocelot/discussions?discussions_q=label%3AIIS>`_, etc.\n\n.. |image-IIS| image:: ../images/label-IIS-c5def5.svg\n  :alt: label IIS\n  :class: img-valign-bottom\n  :target: https://github.com/ThreeMammals/Ocelot/labels/IIS\n.. _IIS-related objects: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20IIS&type=code\n\n.. _hosting-gotchas-kestrel:\n\nKestrel\n-------\n\n  | Repository Label: |image-Kestrel|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/labels/Kestrel}{Kestrel}`\n  | Microsoft Learn: `Kestrel web server in ASP.NET Core <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel>`_\n\nWe **do** recommend to deploy Ocelot app to self-hosting environments, aka Kestrel vs Docker.\nWe try to optimize Ocelot web app for Kestrel & Docker hosting scenarios, but keep in mind the following gotchas.\n\n**1. Upload and download large files** [#f1]_\n\nThis is proxying the large content through the gateway: when you pump large (static) files using the gateway.\nWe believe that your client apps should have direct integration to (static) files persistent storages and services: remote & destributed file systems, CDNs, static files & blob storages, etc.\nWe **do not** recommend to pump large files (100Mb+ or even larger 1GB+) using gateway because of performance reasons: consuming memory and CPU, long delay times, producing network errors for downstream streaming, impact on other routes.\n\n  | The community constanly reports issues related to `large files <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22large+file%22&type=issues>`_, ``application/octet-stream`` content type, :ref:`chunked-encoding`, etc., see issues `749`_, `1472`_.\n  | If you still want to pump large files through an Ocelot gateway instance, use `23.0`_ version and higher.\n  | In case of some errors, see the next point.\n\n**2. Maximum request body size**\n\n    Docs: `Maximum request body size | Configure options for the ASP.NET Core Kestrel web server <https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options#maximum-request-body-size>`_.\n\nASP.NET ``HttpRequest`` behaves erroneously for application instances that do not have their Kestrel `MaxRequestBodySize <https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize>`_ option configured correctly and having pumped large files of unpredictable size which exceeds the limit.\n\nAs a quick fix, use this configuration recipe:\n\n.. code-block:: csharp\n\n    var builder = WebApplication.CreateBuilder(args);\n    builder.WebHost.ConfigureKestrel((context, serverOptions) =>\n    {\n        int myVideoFileMaxSize = 1_073_741_824; // assume your file storage has max file size as 1 GB (1_073_741_824)\n        int totalSize = myVideoFileMaxSize + 26_258_176; // and add some extra size\n        serverOptions.Limits.MaxRequestBodySize = totalSize; // 1_100_000_000 thus 1 GB file should not exceed the limit\n    });\n\n.. _break: http://break.do\n\n    Finally, there is the special |image-Kestrel|:pdf:`\\href{https://github.com/ThreeMammals/Ocelot/labels/Kestrel}{Kestrel}` label for all `Kestrel-related objects <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20Kestrel&type=code>`_.\n    Feel free to put this label onto `issues <https://github.com/ThreeMammals/Ocelot/labels/Kestrel>`__, `pull requests <https://github.com/ThreeMammals/Ocelot/pulls?q=is%3Apr+label%3AKestrel+>`__, `discussions <https://github.com/ThreeMammals/Ocelot/discussions?discussions_q=label%3AKestrel>`__, etc.\n\n.. |image-Kestrel| image:: ../images/label-Kestrel-c5def5.svg\n  :alt: label Kestrel\n  :class: img-valign-bottom\n  :target: https://github.com/ThreeMammals/Ocelot/labels/Kestrel\n\n\"\"\"\"\n\n.. [#f1] Large files pumping is stabilized and available as complete solution starting in `23.0`_ release. We believe our PRs `1724`_, `1769`_ helped to resolve the issues and stabilize large content proxying problems of `22.0.1`_ version and lower.\n.. _22.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/22.0.1\n.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0\n.. _749: https://github.com/ThreeMammals/Ocelot/issues/749\n.. _1472: https://github.com/ThreeMammals/Ocelot/issues/1472\n.. _1657: https://github.com/ThreeMammals/Ocelot/issues/1657\n.. _1630: https://github.com/ThreeMammals/Ocelot/issues/1630\n.. _1724: https://github.com/ThreeMammals/Ocelot/pull/1724\n.. _1769: https://github.com/ThreeMammals/Ocelot/pull/1769\n"
  },
  {
    "path": "docs/introduction/notsupported.rst",
    "content": "Not Supported\r\n=============\r\n\r\nOcelot does not support...\r\n\r\n.. _chunked-encoding:\r\n\r\nChunked Encoding\r\n----------------\r\n\r\nOcelot will always get the body size and return `Content-Length <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length>`_ header.\r\nSorry, if this doesn't work for your use case! \r\n\t\r\nForwarding ``Host`` header\r\n--------------------------\r\n\r\nThe `Host <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host>`_ header that you send to Ocelot will not be forwarded to the downstream service.\r\nObviously this would break everything.\r\n\r\nSwagger\r\n-------\r\n\r\nContributors have looked multiple times at building ``swagger.json`` out of the Ocelot ``ocelot.json`` but it doesnt fit into the vision the team has for Ocelot.\r\nIf you would like to have Swagger in Ocelot then you must roll your own ``swagger.json`` and do the following in your `Program.cs <https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs>`_.\r\nThe code sample below registers a piece of middleware that loads your hand rolled ``swagger.json`` and returns it on ``/swagger/v1/swagger.json``.\r\nIt then registers the SwaggerUI middleware from `Swashbuckle.AspNetCore <https://www.nuget.org/packages/Swashbuckle.AspNetCore>`_ package:\r\n\r\n.. code-block:: csharp\r\n\r\n    var builder = WebApplication.CreateBuilder(args);\r\n    // ...\r\n    var app = builder.Build();\r\n    app.Map(\"/swagger/v1/swagger.json\", builder =>\r\n        builder.Run(async context => {\r\n            var json = await File.ReadAllTextAsync(\"swagger.json\");\r\n            await context.Response.WriteAsync(json);\r\n        }));\r\n    app.UseSwaggerUI(c =>\r\n    {\r\n        c.SwaggerEndpoint(\"/swagger/v1/swagger.json\", \"Ocelot\");\r\n    });\r\n\r\n    await app.UseOcelot();\r\n    await app.RunAsync();\r\n\r\nThe main reasons why we don't think Swagger makes sense is we already hand roll our definition in ``ocelot.json``.\r\nIf we want people developing against Ocelot to be able to see what routes are available then either share the ``ocelot.json`` with them\r\n(This should be as easy as granting access to a repo etc) or use the Ocelot :doc:`../features/administration` API so that they can query Ocelot for the configuration.\r\n\r\nIn addition to this, many people will configure Ocelot to proxy all traffic like ``/products/{everything}`` to their product service\r\nand you would not be describing what is actually available if you parsed this and turned it into a Swagger path.\r\nAlso Ocelot has no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return multiple models.\r\nOcelot does not know what models might be used in POST, PUT etc, so it all gets a bit messy, and finally, the Swashbuckle package **does not** reload ``swagger.json`` if it changes during runtime.\r\nOcelot's configuration can change during runtime so the Swagger and Ocelot information would not match.\r\nUnless we rolled our own Swagger implementation.\r\n\r\nIf the developer wants something to easily test against the Ocelot API then we suggest using `Postman <https://www.postman.com/>`_ as a simple way to do this.\r\nIt might even be possible to write something that maps ``ocelot.json`` to the Postman JSON spec. However we do not intend to do this.\r\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo The 'sphinx-build' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the 'sphinx-build' executable.\r\n\techo Alternatively you may add the Sphinx directory to PATH.\r\n\techo If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nset command=\"%1\" &:: html, clean and etc.\r\ncall :dequote %command%\r\necho Doing %ret% ...\r\n\r\nIF %command% == \"\" (\r\n   set status=\"FAILED\"\r\n   echo There is no build command! Available commands: clean, html\r\n   echo See Sphinx Help below.\r\n   %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n) ELSE (\r\n   %SPHINXBUILD% -M %command% %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n   set status=\"DONE\"\r\n)\r\ncall :dequote %status%\r\necho Build %ret%\r\n\r\npopd\r\n\r\n:dequote\r\nsetlocal\r\nset thestring=%~1\r\nendlocal&set ret=%thestring%\r\ngoto :eof\r\n"
  },
  {
    "path": "docs/make.ps1",
    "content": "# Command file for Sphinx documentation\n# PowerShell version without env var usage\n\nparam (\n    [string]$command\n)\n\n# Define sphinx-build as a normal variable\n$sphinxBuild = \"sphinx-build\"\n\n$SOURCEDIR = \".\"\n$BUILDDIR = \"_build\"\n\nWrite-Host \"Doing $command ...\"\n\nif ([string]::IsNullOrEmpty($command)) {\n    $status = \"FAILED\"\n    Write-Host \"There is no build command! Available commands: clean, html\"\n    Write-Host \"See Sphinx Help below.\"\n    & $sphinxBuild -M help $SOURCEDIR $BUILDDIR $SPHINXOPTS $O\n} else {\n    & $sphinxBuild -M $command $SOURCEDIR $BUILDDIR $SPHINXOPTS $O\n    $status = \"DONE\"\n}\n\nWrite-Host \"Build $status\"\n"
  },
  {
    "path": "docs/make.sh",
    "content": "#!/bin/sh\n#\n# Command file for Sphinx documentation\n#\nif [ \"$SPHINXBUILD\" == \"\" ]\nthen\n   SPHINXBUILD=\"sphinx-build\"\nfi\n\nSOURCEDIR=\".\"\nBUILDDIR=\"_build\"\n\ncommand=$1 # html, clean and etc.\necho Doing $command ...\nif [ \"$command\" == \"\" ]\nthen\n   status=\"FAILED\"\n   echo There is no build command! Available commands: clean, html\n   echo See Sphinx Help below.\n   $SPHINXBUILD -M help $SOURCEDIR $BUILDDIR $SPHINXOPTS $O\nelse\n   $SPHINXBUILD -M $command $SOURCEDIR $BUILDDIR $SPHINXOPTS $O\n   status=\"DONE\"\nfi\necho Build $status\n"
  },
  {
    "path": "docs/readme.md",
    "content": "# Ocelot Documentation\r\n\r\nThe folder contains the documentation for Ocelot, build tools and configuration.\r\n\r\nWe are using [Read the Docs](https://about.readthedocs.com) to host the documentation and the rendered version can be found [here](https://ocelot.readthedocs.io).\r\n\r\nDoc pages are authored in [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html) (reST).\r\nYou can find a primer [here](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html).\r\n\r\nYou can find more information about [reST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/) markup and [Sphinx](https://github.com/sphinx-doc/sphinx) under the following links:\r\n* [Read the Docs documentation](https://docs.readthedocs.io)\r\n* [GitHub: The Sphinx documentation generator](https://github.com/sphinx-doc/sphinx)\r\n* [Sphinx documentation](https://www.sphinx-doc.org/)\r\n* [YouTube: Sphinx & Read the Docs by Mahdi Yusuf](https://www.youtube.com/watch?v=oJsUvBQyHBs)\r\n"
  },
  {
    "path": "docs/releasenotes.rst",
    "content": ".. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\n.. _24.1.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\n.. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0\n.. _25.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0\n.. _.NET 9: https://dotnet.microsoft.com/en-us/download/dotnet/9.0\n.. _.NET 10: https://github.com/ThreeMammals/Ocelot/milestone/13\n.. _Globality: https://github.com/ThreeMammals/Ocelot/milestone/9\n.. _Ocelot: https://www.nuget.org/packages/Ocelot\n.. role::  htm(raw)\n    :format: html\n\n.. _welcome:\n\n#######\nWelcome\n#######\n\nWelcome to the Ocelot `25.0`_ documentation!\n\nIt is recommended to read all :ref:`release-notes` if you have deployed the Ocelot app in a production environment and are planning to upgrade to major, minor or patched versions.\n\n.. The major version `25.0.0`_ includes several patches, the history of which is outlined below.\n\n.. .. admonition:: Patches\n\n..   - `24.1.1`_, on July 16, 2025: Issue `2299`_ patch ...\n\n.. _release-notes:\n\nRelease Notes\n-------------\n.. _Ocelot.Provider.Kubernetes: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes/\n.. _Obsolete attributes: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20%5BObsolete&type=code\n\n  | Release Tag: `25.0.0`_\n  | Release Codename: `.NET 10`_\n\n.. In this minor release, the Ocelot team put the spotlight on the :doc:`../features/configuration` feature as part of their semi-annual 2025 effort, with a particular focus on the :ref:`config-global-configuration-schema`.\n.. This release enhances support for global configurations across both routing modes: the classic static :doc:`../features/routing` and the :doc:`service discovery <../features/servicediscovery>`-based :ref:`Dynamic Routing <sd-dynamic-routing>`.\n\n.. The updated documentation highlights `the deprecation <https://ocelot.readthedocs.io/en/latest/search.html?q=deprecated>`_ of certain options through multiple notes and warnings.\n.. This deprecation process will be completed in the upcoming `.NET 10`_ release.\n.. With the `Obsolete attributes`_ in place, C# developers will notice several warnings in the build logs during compilation.\n\n.. On top of that, this release brings a great enhancement to the :doc:`../features/kubernetes` provider, also known as the `Ocelot.Provider.Kubernetes`_ package.\n\n.. What's New?\n.. -----------\n.. .. _@raman-m: https://github.com/raman-m\n.. .. _@kick2nick: https://github.com/kick2nick\n.. .. _@hogwartsdeveloper: https://github.com/hogwartsdeveloper\n.. .. _@RaynaldM: https://github.com/RaynaldM\n.. .. _585: https://github.com/ThreeMammals/Ocelot/issues/585\n.. .. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073\n.. .. _2081: https://github.com/ThreeMammals/Ocelot/pull/2081\n.. .. _2174: https://github.com/ThreeMammals/Ocelot/pull/2174\n.. .. _Dynamic routing global configuration: https://github.com/ThreeMammals/Ocelot/issues/585\n.. .. _KubeClient: https://www.nuget.org/packages/KubeClient/\n.. .. _Polly: https://www.nuget.org/packages/Polly/\n.. .. _Ocelot.Provider.Polly: https://www.nuget.org/packages/Ocelot.Provider.Polly\n.. .. _FailureRatio and SamplingDuration parameters of Polly V8 circuit-breaker: https://github.com/ThreeMammals/Ocelot/issues/2080\n\n.. - :doc:`../features/configuration`: The \"`Dynamic routing global configuration`_\" feature has been redesigned by `@raman-m`_ and contributors.\n\n..   This update brings changes to the :ref:`config-dynamic-route-schema` and :ref:`config-global-configuration-schema`, while the :ref:`config-route-schema` stays the same apart from deprecation updates.\n..   All work was coordinated under issue `585`_, which addressed the challenges of configuring Ocelot's most popular features globally before version `24.1`_, when :ref:`dynamic routing <sd-dynamic-routing>` gained global configuration partial support, but static routing mostly lacked it.\n..   A key outcome of `585`_ is the ability to override global configuration options within the ``DynamicRoutes`` collection.\n..   This ongoing issue will continue to require attention, as adapting static route global configurations for :ref:`dynamic routing <sd-dynamic-routing>` is complex and, in some cases, impossible.\n..   This will be a challenge for future `Ocelot`_ releases and the community.\n\n.. - :doc:`../features/kubernetes`: The \":ref:`Kubernetes provider based on watch requests <k8s-watchkube-provider>`\" feature by `@kick2nick`_ in pull request `2174`_.\n\n..   The `Ocelot.Provider.Kubernetes`_ package now features a new :ref:`WatchKube provider <k8s-watchkube-provider>` for :doc:`Kubernetes <../features/kubernetes>` service discovery.\n..   This provider is a great fit for high-load environments where the older :ref:`Kube <k8s-kube-provider>` and :ref:`PollKube <k8s-pollkube-provider>` providers struggle to handle heavy traffic, often leading to increased log errors, HTTP 500 issues, and potential Ocelot instance failures.\n..   ``WatchKube`` is the next step in the evolution of these providers, leveraging the reactive capabilities of the `KubeClient`_ API.\n..   For guidance on choosing the right provider for your Kubernetes setup, check out the \":ref:`k8s-comparing-providers`\" section.\n\n.. - :doc:`../features/configuration`: The \":ref:`Routing default timeout <config-timeout>`\" feature by `@hogwartsdeveloper`_ in pull request `2073`_.\n\n..   In the past, the ``Timeout`` setting in the :ref:`config-route-schema` did not actually stop requests, defaulting instead to a fixed `90 seconds <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%2290+seconds%22&type=code>`_.\n..   Custom timeouts were handled using the :doc:`../features/qualityofservice` :ref:`qos-timeout-strategy`, and this only applied if `Polly`_ and the `Ocelot.Provider.Polly`_ package were used.\n..   Now, the ``Timeout`` option (in seconds) can be set at the route, global, and QoS levels.\n..   The :ref:`config-global-configuration-schema` and :ref:`config-dynamic-route-schema` also include the new ``Timeout`` setting, making it possible to configure default timeouts for :ref:`dynamic routing <sd-dynamic-routing>` as well.\n\n.. - :doc:`../features/qualityofservice`: The \"`FailureRatio and SamplingDuration parameters of Polly V8 circuit-breaker`_\" feature by `@RaynaldM`_ in pull request `2081`_.\n\n..   Starting with version `24.1`_, two new options in :ref:`qos-schema`, ``FailureRatio`` and ``SamplingDuration``, let you fine-tune the behavior of the :ref:`qos-circuit-breaker-strategy`.\n..   Both can be :ref:`configured globally <qos-global-configuration>`, even with :ref:`dynamic routing <sd-dynamic-routing>`.\n\n..   .. note:: The ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue`` options are now deprecated in `24.1`_, so check the \":ref:`qos-schema`\" documentation for details.\n\n.. What's Updated?\n.. ---------------\n.. .. _@marklonquist: https://github.com/marklonquist\n.. .. _@jlukawska: https://github.com/jlukawska\n.. .. _@MiladRv: https://github.com/MiladRv\n.. .. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592\n.. .. _1659: https://github.com/ThreeMammals/Ocelot/pull/1659\n.. .. _2114: https://github.com/ThreeMammals/Ocelot/pull/2114\n.. .. _2294: https://github.com/ThreeMammals/Ocelot/pull/2294\n.. .. _2295: https://github.com/ThreeMammals/Ocelot/pull/2295\n.. .. _2324: https://github.com/ThreeMammals/Ocelot/pull/2324\n.. .. _2331: https://github.com/ThreeMammals/Ocelot/pull/2331\n.. .. _2332: https://github.com/ThreeMammals/Ocelot/pull/2332\n.. .. _2336: https://github.com/ThreeMammals/Ocelot/pull/2336\n.. .. _2339: https://github.com/ThreeMammals/Ocelot/pull/2339\n.. .. _2342: https://github.com/ThreeMammals/Ocelot/pull/2342\n.. .. _2345: https://github.com/ThreeMammals/Ocelot/pull/2345\n.. .. _2347: https://github.com/ThreeMammals/Ocelot/pull/2347\n.. .. _File-model: https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/Configuration/File\n.. .. _deprecated options: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+deprecated+language%3AreStructuredText&type=code&l=reStructuredText\n.. .. _Ocelot.Testing: https://github.com/ThreeMammals/Ocelot/tree/24.0.0/test/Ocelot.Testing\n.. .. _extension packages: https://www.nuget.org/profiles/ThreeMammals\n.. .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0\n.. .. _DevOps: https://github.com/ThreeMammals/Ocelot/labels/DevOps\n.. .. _GH-Actions: https://github.com/ThreeMammals/Ocelot/actions\n\n.. - :doc:`../features/configuration`: Several `File-model`_ options have been deprecated by `@raman-m`_.\n\n..   The updated docs now highlight these `deprecated options`_ with multiple notes and warnings.\n..   The `24.1`_ deprecation process will wrap up in the upcoming `.NET 10`_ release.\n..   Due to the `Obsolete attributes`_, C# developers will notice several build warnings during compilation.\n\n.. - :ref:`b-testing`: The `Ocelot.Testing`_ project was deprecated by `@raman-m`_ in pull request `2295`_.\n\n..   The project was removed from the main repo and moved to its own `Ocelot.Testing <https://github.com/ThreeMammals/Ocelot.Testing>`__ repository.\n..   This change allows the `Ocelot.Testing <https://www.nuget.org/packages/Ocelot.Testing/>`__ package to be shared independently for delivery of `extension packages`_.\n..   The Ocelot team also plans to deprecate more projects and move them to separate repos because:\n..   **a)** despite the fact that a monorepo enables faster builds and quicker delivery;\n..   **b)** but the release process can be delayed by missing versions of integrated libraries in `extension packages`_.\n..   The goal is for the Ocelot repo to only contain essential projects, avoiding delays caused by integrated package release schedules.\n..   Legacy or abandoned integrated packages should be deprecated and maintained in their own repos with independent release cycles.\n\n.. - :doc:`../features/headerstransformation`: Added :ref:`global configuration <ht-configuration>` by `@marklonquist`_ in pull request `1659`_.\n\n..   The :ref:`config-global-configuration-schema` now includes new ``DownstreamHeaderTransform`` and ``UpstreamHeaderTransform`` options.\n..   These work only with static routes, meaning the ``Routes`` collection (see :ref:`config-route-schema`).\n..   They are not supported for dynamic routes because they are not part of the :ref:`config-dynamic-route-schema`, and Ocelot Core does not read global configuration of this feature in :ref:`dynamic routing <sd-dynamic-routing>` mode.\n..   This is noted in the :ref:`ht-roadmap` documentation.\n\n.. - :doc:`../features/authentication`: Added :ref:`global configuration <authentication-configuration>` by `@jlukawska`_ in pull request `2114`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``AuthenticationOptions`` property for setting up static routes globally.\n..   This also introduces the :ref:`AllowAnonymous boolean option <authentication-configuration>` within ``AuthenticationOptions`` to control static route authentication.\n..   Later, pull request `2336`_ extended global authentication support to dynamic routes.\n\n..   .. note:: The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_—see the \":ref:`authentication-options-schema`\" documentation for details.\n\n.. - :doc:`../features/ratelimiting`: Re-designed :ref:`global configuration <rl-configuration>` by `@MiladRv`_ and `@raman-m`_ in pull request `2294`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``RateLimitOptions`` property for both static and dynamic routes.\n..   Previously, global configuration was available through ``RateLimitOptions`` in :ref:`dynamic routing <sd-dynamic-routing>` mode, while route overriding used the now-deprecated ``RateLimitRule`` from the :ref:`config-dynamic-route-schema`.\n\n..   This marks the second major overhaul of the *Rate Limiting* feature since the first update in pull request `1592`_.\n..   A new ``Wait`` option has been added, replacing the deprecated ``PeriodTimespan``, to enhance the :ref:`Fixed Window <rl-algorithms>` algorithm.\n..   The full list of deprecated options can be found in the \":ref:`Deprecated options <rl-deprecated-options>`\" documentation.\n\n.. - :doc:`../features/loadbalancer`: Added :ref:`global configuration <lb-global-configuration>` by `@raman-m`_ in pull request `2324`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``LoadBalancerOptions`` property for both static and dynamic routes.\n..   Previously, global configuration was available through ``LoadBalancerOptions`` in :ref:`dynamic routing <sd-dynamic-routing>` mode without dynamic route overrides.\n..   Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``LoadBalancerOptions`` for overriding, and global configuration for static routes is now supported as well.\n\n.. - :doc:`../features/caching`: Added :ref:`global configuration <caching-global-configuration>` by `@raman-m`_ in pull request `2331`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``CacheOptions`` property for both static and dynamic routes.\n..   Global configuration has been available for static routes since version `23.3`_, but starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``CacheOptions`` for overriding.\n\n..   .. note::\n..     The ``FileCacheOptions`` property in the :ref:`config-route-schema` (static routes) is deprecated in version `24.1`_.\n..     For more details, see the caching :ref:`caching-configuration` documentation.\n\n.. - :ref:`Http Handler <config-http-handler-options>`: Added :ref:`global configuration <config-http-handler-options>` by `@raman-m`_ in pull request `2332`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``HttpHandlerOptions`` property for both static and dynamic routes.\n..   Previously, global configuration was available through ``HttpHandlerOptions`` in :ref:`dynamic routing <sd-dynamic-routing>` mode without dynamic route overriding.\n..   Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``HttpHandlerOptions`` for overriding, and global configuration is now available for static routes as well.\n\n.. - :doc:`../features/authentication`: Added :ref:`global configuration <authentication-global-configuration>` by `@raman-m`_ in pull request `2336`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``AuthenticationOptions`` property for both static and dynamic routes.\n..   Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``AuthenticationOptions`` to override global settings.\n\n..   .. note::\n..     The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_, so check the \":ref:`authentication-options-schema`\" documentation for details.\n\n.. - :doc:`../features/qualityofservice`: Added :ref:`global configuration <qos-global-configuration>` by `@raman-m`_ in pull request `2339`_.\n\n..   The :ref:`config-global-configuration-schema` now includes a new ``QoSOptions`` property for both static and dynamic routes.\n..   Previously, global configuration was available through ``QoSOptions`` in :ref:`dynamic routing <sd-dynamic-routing>` mode without the option for dynamic route overrides.\n..   Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` supports ``QoSOptions`` for overriding, and global configuration support is now available for static routes as well.\n\n..   .. note::\n..     The ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue`` options are deprecated in version `24.1`_.\n..     For details, see the \":ref:`qos-schema`\" documentation.\n\n.. - `DevOps`_: Stabilized tests and reviewed `GH-Actions`_ workflows by `@raman-m`_ in pull requests `2342`_ and `2345`_.\n\n..   These efforts kept the CI/CD builds in `GitHub Actions <https://github.com/ThreeMammals/Ocelot/actions>`_ stable, targeting the `alpha release <https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0-pre-release-24-1.1>`_ of version `24.1`_.\n..   The CI/CD environment was set up and tested `GH-Actions`_ workflows in advance for the beta release, which is the goal of pull request `2347`_.\n\n.. Patches Included\n.. ----------------\n.. .. _@mehyaa: https://github.com/mehyaa\n.. .. _913: https://github.com/ThreeMammals/Ocelot/issues/913\n.. .. _930: https://github.com/ThreeMammals/Ocelot/issues/930\n.. .. _1478: https://github.com/ThreeMammals/Ocelot/pull/1478\n.. .. _2091: https://github.com/ThreeMammals/Ocelot/pull/2091\n.. .. _2304: https://github.com/ThreeMammals/Ocelot/issues/2304\n.. .. _2335: https://github.com/ThreeMammals/Ocelot/pull/2335\n.. .. _RFC 8693: https://datatracker.ietf.org/doc/html/rfc8693\n\n.. - :doc:`../features/websockets`: Issue `930`_ patch by `@hogwartsdeveloper`_ in pull request `2091`_.\n\n..   This update removes the troublesome ``System.Net.WebSockets.WebSocketException`` from logs, preventing Ocelot from running into 500 status disasters.\n..   The issue stemmed from client-side or network events that Ocelot's ``WebSocketsProxyMiddleware`` could not anticipate on the server side.\n..   The patch now checks for incorrect connection statuses, attempting to close the connection and end server-side tasks gracefully without errors.\n\n.. - :doc:`../features/kubernetes`: Issue `2304`_ patch by `@raman-m`_ in pull request `2335`_.\n\n..   This update fixes the :ref:`PollKube provider <k8s-pollkube-provider>` to address a bug with the first cold request, where the winning thread got an empty collection before the initial callback was triggered.\n..   The solution is to call the integrated discovery provider for the first cold request when the queue is empty.\n\n.. - :doc:`../features/authorization`: Issue `913`_ patch by `@mehyaa`_ in pull request `1478`_.\n\n..   Starting with version `24.1`_, Ocelot now supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the '``scope``' claim in the ``ScopesAuthorizer`` service, also referred to as the ``IScopesAuthorizer`` service in the DI container.\n..   This is noted in the \":ref:`authentication-allowed-scopes`\" documentation (see the first note).\n\nContributing\n------------\n\n.. |octocat| image:: images/octocat.png\n  :alt: octocat\n  :height: 25\n  :class: img-valign-middle\n  :target: https://github.com/ThreeMammals/Ocelot/\n.. _Pull requests: https://github.com/ThreeMammals/Ocelot/pulls\n.. _issues: https://github.com/ThreeMammals/Ocelot/issues\n.. _Ocelot GitHub: https://github.com/ThreeMammals/Ocelot/\n.. _Ocelot Discussions: https://github.com/ThreeMammals/Ocelot/discussions\n.. _ideas: https://github.com/ThreeMammals/Ocelot/discussions/categories/ideas\n.. _questions: https://github.com/ThreeMammals/Ocelot/discussions/categories/q-a\n\n`Pull requests`_, `issues`_, and commentary are welcome at the `Ocelot GitHub`_ repository.\nFor `ideas`_ and `questions`_, please post them in the `Ocelot Discussions`_ space. |octocat|\n\nOur :doc:`../building/devprocess` is a part of successful :doc:`../building/releaseprocess`.\nIf you are a new contributor, it is crucial to read :doc:`../building/devprocess` attentively to grasp our methods for efficient and swift feature delivery.\nWe, as a team, advocate adhering to :ref:`dev-best-practices` throughout the development phase.\n\nWe extend our best wishes for your successful contributions to the Ocelot product! |octocat|\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "# Defining the exact version will make sure things don't break\nsphinx==9.0.4\nalabaster==1.0.0\nsphinx_copybutton==0.5.2\n"
  },
  {
    "path": "postman/ocelot.postman_collection.json",
    "content": "{\n\t\"id\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\n\t\"name\": \"Ocelot\",\n\t\"description\": \"\",\n\t\"order\": [\n\t\t\"a1c95935-ed18-d5dc-bcb8-a3db8ba1934f\",\n\t\t\"ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2\",\n\t\t\"c4494401-3985-a5bf-71fb-6e4171384ac6\",\n\t\t\"09af8dda-a9cb-20d2-5ee3-0a3023773a1a\",\n\t\t\"e8825dc3-4137-99a7-0000-ef5786610dc3\",\n\t\t\"fddfc4fa-5114-69e3-4744-203ed71a526b\",\n\t\t\"c45d30d7-d9c4-fa05-8110-d6e769bb6ff9\",\n\t\t\"4684c2fa-f38c-c193-5f55-bf563a1978c6\",\n\t\t\"5f308240-79e3-cf74-7a6b-fe462f0d54f1\",\n\t\t\"178f16da-c61b-c881-1c33-9d64a56851a4\",\n\t\t\"26a08569-85f6-7f9a-726f-61be419c7a34\"\n\t],\n\t\"folders\": [],\n\t\"timestamp\": 0,\n\t\"owner\": \"212120\",\n\t\"public\": false,\n\t\"requests\": [\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"09af8dda-a9cb-20d2-5ee3-0a3023773a1a\",\n\t\t\t\"name\": \"GET http://localhost:5000/comments?postId=1\",\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"data\": null,\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/comments?postId=1\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"id\": \"178f16da-c61b-c881-1c33-9d64a56851a4\",\n\t\t\t\"headers\": \"Authorization: Bearer {{AccessToken}}\\n\",\n\t\t\t\"url\": \"http://localhost:5000/administration/configuration\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"pathVariables\": {},\n\t\t\t\"method\": \"GET\",\n\t\t\t\"data\": null,\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"time\": 1508914722969,\n\t\t\t\"name\": \"GET http://localhost:5000/admin/configuration\",\n\t\t\t\"description\": \"\",\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"id\": \"26a08569-85f6-7f9a-726f-61be419c7a34\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"url\": \"http://localhost:5000/administration/connect/token\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"pathVariables\": {},\n\t\t\t\"method\": \"POST\",\n\t\t\t\"data\": [\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\"value\": \"raft\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\"value\": \"REALLYHARDPASSWORD\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"scope\",\n\t\t\t\t\t\"value\": \"admin raft \",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\"value\": \"admin\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": false\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"password\",\n\t\t\t\t\t\"value\": \"secret\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": false\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\"value\": \"client_credentials\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"tests\": \"var jsonData = JSON.parse(responseBody);\\npostman.setGlobalVariable(\\\"AccessToken\\\", jsonData.access_token);\\npostman.setGlobalVariable(\\\"RefreshToken\\\", jsonData.refresh_token);\",\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"time\": 1513240031907,\n\t\t\t\"name\": \"POST http://localhost:5000/admin/connect/token copy copy\",\n\t\t\t\"description\": \"\",\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"4684c2fa-f38c-c193-5f55-bf563a1978c6\",\n\t\t\t\"name\": \"DELETE http://localhost:5000/posts/1\",\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"data\": null,\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"DELETE\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts/1\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"id\": \"5f308240-79e3-cf74-7a6b-fe462f0d54f1\",\n\t\t\t\"headers\": \"Authorization: Bearer {{AccessToken}}\\n\",\n\t\t\t\"url\": \"http://localhost:5000/administration/.well-known/openid-configuration\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"pathVariables\": {},\n\t\t\t\"method\": \"GET\",\n\t\t\t\"data\": null,\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": \"{}\",\n\t\t\t\"time\": 1488038888813,\n\t\t\t\"name\": \"GET http://localhost:5000/admin/.well-known/openid-configuration\",\n\t\t\t\"description\": \"\",\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\n\t\t\t\"folder\": null,\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": null,\n\t\t\t\"queryParams\": [],\n\t\t\t\"headerData\": [\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\"value\": \"Bearer {{AccessToken}}\",\n\t\t\t\t\t\"description\": \"\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"pathVariableData\": []\n\t\t},\n\t\t{\n\t\t\t\"id\": \"a1c95935-ed18-d5dc-bcb8-a3db8ba1934f\",\n\t\t\t\"folder\": null,\n\t\t\t\"name\": \"GET http://localhost:5000/posts\",\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"data\": [\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"client_id\",\n\t\t\t\t\t\"value\": \"admin\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"client_secret\",\n\t\t\t\t\t\"value\": \"secret\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"scope\",\n\t\t\t\t\t\"value\": \"admin\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"username\",\n\t\t\t\t\t\"value\": \"admin\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"password\",\n\t\t\t\t\t\"value\": \"admin\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"key\": \"grant_type\",\n\t\t\t\t\t\"value\": \"password\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"POST\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/admin/configuration\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": \"{}\",\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"c4494401-3985-a5bf-71fb-6e4171384ac6\",\n\t\t\t\"name\": \"GET http://localhost:5000/posts/1/comments\",\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"data\": null,\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts/1/comments\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"c45d30d7-d9c4-fa05-8110-d6e769bb6ff9\",\n\t\t\t\"name\": \"PATCH http://localhost:5000/posts/1\",\n\t\t\t\"dataMode\": \"raw\",\n\t\t\t\"data\": [],\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"PATCH\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts/1\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\n\t\t\t\"rawModeData\": \"{\\n  \\\"title\\\": \\\"gfdgsgsdgsdfgsdfgdfg\\\",\\n}\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"e8825dc3-4137-99a7-0000-ef5786610dc3\",\n\t\t\t\"name\": \"POST http://localhost:5000/posts/1\",\n\t\t\t\"dataMode\": \"raw\",\n\t\t\t\"data\": [],\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"POST\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\n\t\t\t\"rawModeData\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"title\\\": \\\"test\\\",\\n  \\\"body\\\": \\\"test\\\"\\n}\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2\",\n\t\t\t\"name\": \"GET http://localhost:5000/posts/1\",\n\t\t\t\"dataMode\": \"params\",\n\t\t\t\"data\": null,\n\t\t\t\"rawModeData\": null,\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts/1\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\n\t\t},\n\t\t{\n\t\t\t\"folder\": null,\n\t\t\t\"id\": \"fddfc4fa-5114-69e3-4744-203ed71a526b\",\n\t\t\t\"name\": \"PUT http://localhost:5000/posts/1\",\n\t\t\t\"dataMode\": \"raw\",\n\t\t\t\"data\": [],\n\t\t\t\"descriptionFormat\": \"html\",\n\t\t\t\"description\": \"\",\n\t\t\t\"headers\": \"\",\n\t\t\t\"method\": \"PUT\",\n\t\t\t\"pathVariables\": {},\n\t\t\t\"url\": \"http://localhost:5000/posts/1\",\n\t\t\t\"preRequestScript\": null,\n\t\t\t\"tests\": null,\n\t\t\t\"currentHelper\": \"normal\",\n\t\t\t\"helperAttributes\": {},\n\t\t\t\"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\n\t\t\t\"rawModeData\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"title\\\": \\\"test\\\",\\n  \\\"body\\\": \\\"test\\\"\\n}\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "samples/Basic/API.http",
    "content": "@OcelotHttp = http://localhost:5555\n\nGET {{OcelotHttp}}/ocelot/posts/1\nAccept: application/json\n###\nGET {{OcelotHttp}}/ocelot/docs/\nAccept: text/html, */*\n###\n\n@OcelotHttps = https://localhost:7777\n\nGET {{OcelotHttps}}/ocelot/posts/3\nAccept: application/json\n###\nGET {{OcelotHttps}}/ocelot/docs/releasenotes.html\nAccept: text/html, */*\n###\n"
  },
  {
    "path": "samples/Basic/Ocelot.Samples.Basic.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Basic</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Basic/Program.cs",
    "content": "using Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Ocelot Basic setup\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot(); // single ocelot.json file in read-only mode\nbuilder.Services\n    .AddOcelot(builder.Configuration);\n\n// Add your features\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\n// Add middlewares aka app.Use*()\nvar app = builder.Build();\nawait app.UseOcelot();\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/Basic/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"ocelot/posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5555\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"ocelot/docs/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7777;http://localhost:5555\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"ocelot/posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7777/ocelot/docs/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7777;http://localhost:5555\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62555/\",\n      \"sslPort\": 44355\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Basic/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Basic/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/Basic/ocelot.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/posts/{id}\",\n      \"DownstreamPathTemplate\": \"/todos/{id}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 443\n        }\n      ]\n    },\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/docs/{everything}\",\n      \"DownstreamPathTemplate\": \"/en/latest/{everything}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ]\n    },\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ]\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"BaseUrl\": \"http://localhost:5555\" // \"https://localhost:7777\"\n  }\n}\n"
  },
  {
    "path": "samples/Basic/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Configuration/API.http",
    "content": "@HttpHost = http://localhost:5556\n\nGET {{HttpHost}}/ocelot/posts/1\nAccept: application/json\n###\nGET {{HttpHost}}/ocelot/docs/\nAccept: text/html, */*\n###\nGET {{HttpHost}}/weather/current/London\nAccept: application/json\n###\n\n@HttpsHost = https://localhost:7778\n\nGET {{HttpsHost}}/ocelot/posts/3\nAccept: application/json\n###\nGET {{HttpsHost}}/ocelot/docs/releasenotes.html\nAccept: text/html, */*\n###\nGET {{HttpsHost}}/weather/current/Paris\nAccept: application/json\n###\n"
  },
  {
    "path": "samples/Configuration/Ocelot.Samples.Configuration.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Configuration</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Configuration/Program.cs",
    "content": "using Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Ocelot Basic setup\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot(\"ocelot-configuration\", builder.Environment); // multiple environment files (ocelot.*.json) to be merged to ocelot.json file and write it back to disk\n    //.AddOcelot(\"ocelot-configuration\", builder.Environment, MergeOcelotJson.ToMemory); // to be merged to ocelot.json JSON-data and keep it in memory\nbuilder.Services\n    .AddOcelot(builder.Configuration);\n\n// Add your features\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\n// Add middlewares aka app.Use*()\nvar app = builder.Build();\nawait app.UseOcelot();\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/Configuration/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": false,\n      \"applicationUrl\": \"http://localhost:5556\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": false,\n      \"applicationUrl\": \"https://localhost:7778;http://localhost:5556\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Configuration/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Configuration/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/Configuration/ocelot-configuration/ocelot.docs.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/docs/{everything}\",\n      \"DownstreamPathTemplate\": \"/en/latest/{everything}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ]\n    },\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "samples/Configuration/ocelot-configuration/ocelot.global.json",
    "content": "{\n  \"GlobalConfiguration\": {\n    \"BaseUrl\": \"http://localhost:5556\" // \"https://localhost:7778\"\n  }\n}\n"
  },
  {
    "path": "samples/Configuration/ocelot-configuration/ocelot.posts.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/posts/{id}\",\n      \"DownstreamPathTemplate\": \"/todos/{id}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 443\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "samples/Configuration/ocelot-configuration/ocelot.weather.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/weather/current/{city}\",\n      \"DownstreamPathTemplate\": \"/v1/current.json?q={city}&key=4ea9a1d2aafe4e15bbd173615242312\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"api.weatherapi.com\",\n          \"Port\": 443\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "samples/Configuration/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Eureka</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot.Provider.Eureka\\Ocelot.Provider.Eureka.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot.Provider.Polly\\Ocelot.Provider.Polly.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Eureka/ApiGateway/Program.cs",
    "content": "﻿using Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Provider.Eureka;\nusing Ocelot.Provider.Polly;\nusing Ocelot.Samples.Web;\n\n//_ = OcelotHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\n\n// Ocelot Basic setup\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\nbuilder.Services\n    .AddOcelot(builder.Configuration)\n    .AddEureka()\n    .AddPolly();\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nawait app.UseOcelot();\napp.Run();\n"
  },
  {
    "path": "samples/Eureka/ApiGateway/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"Category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5557\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7779;http://localhost:5557\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7779/category/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7779;http://localhost:5557\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62557/\",\n      \"sslPort\": 44357\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/ApiGateway/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Trace\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    }\n  },\n  \"spring\": {\n    \"application\": { \"name\": \"Ocelot-Gateway\" },\n    \"cloud\": {\n      \"config\": {\n        \"uri\": \"http://localhost:5000\",\n        \"validateCertificates\": false\n      }\n    }\n  },\n  \"eureka\": {\n    \"client\": {\n      \"serviceUrl\": \"http://localhost:8761/eureka/\",\n      \"shouldRegisterWithEureka\": false,\n      \"validateCertificates\": false\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/ApiGateway/ocelot.json",
    "content": "﻿{\n  \"Routes\": [\n    {\n      \"ServiceName\": \"ncore-rat\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/category\",\n      \"DownstreamPathTemplate\": \"/api/category\",\n      \"DownstreamScheme\": \"http\",\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10000,\n        \"TimeoutValue\": 5000\n      },\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    },\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/nodiscovery/category\",\n      \"DownstreamPathTemplate\": \"/api/category\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"localhost\",\n          \"Port\": 5558\n        }\n      ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10000,\n        \"TimeoutValue\": 5000\n      },\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"RequestIdKey\": \"OcRequestId\",\n    \"AdministrationPath\": \"/administration\",\n    \"ServiceDiscoveryProvider\": { \"Type\": \"Eureka\" }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/ApiGateway/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Eureka/DownstreamService/Controllers/CategoryController.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc;\n\nnamespace Ocelot.Samples.Eureka.DownstreamService.Controllers;\n\n[Route(\"api/[controller]\")]\npublic class CategoryController : Controller\n{\n    // GET api/category\n    [HttpGet]\n    public IEnumerable<string> Get()\n    {\n        return new[] { \"category1\", \"category2\" };\n    }\n}\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Eureka</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Steeltoe.Discovery.ClientCore\" Version=\"3.3.0\" />\n    <PackageReference Include=\"System.Text.Json\" Version=\"10.0.5\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/Program.cs",
    "content": "﻿using Steeltoe.Discovery.Client;\n\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.Services\n    .AddDiscoveryClient(builder.Configuration)\n    .AddControllers();\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nif (app.Environment.IsDevelopment())\n{\n    app.UseDeveloperExceptionPage();\n}\napp.UseHttpsRedirection()\n    .UseAuthorization();\napp.MapControllers();\napp.Run();\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"api/Category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5558\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"api/category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7780;http://localhost:5558\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"api/category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7780/api/category\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7780;http://localhost:5558\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62558/\",\n      \"sslPort\": 44358\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/appsettings.Development.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": { \"Default\": \"Warning\" }\n  },\n  \"spring\": {\n    \"application\": { \"name\": \"ncore-rat\" },\n    \"cloud\": {\n      \"config\": {\n        \"uri\": \"http://localhost:5001\",\n        \"validate_certificates\": false\n      }\n    }\n  },\n  \"eureka\": {\n    \"client\": {\n      \"serviceUrl\": \"http://localhost:8761/eureka/\",\n      \"shouldFetchRegistry\": false,\n      \"validateCertificates\": false\n    },\n    \"instance\": { \"port\": 5001 }\n  }\n}\n"
  },
  {
    "path": "samples/Eureka/DownstreamService/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\"\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\"\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Eureka/OcelotEureka.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio 15\nVisualStudioVersion = 15.0.27428.2027\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"DownstreamService\", \"./DownstreamService/DownstreamService.csproj\", \"{2982C147-9446-47FE-862E-E689B64CC7E7}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"ApiGateway\", \"./ApiGateway/ApiGateway.csproj\", \"{006CF27E-5400-43E9-B511-C54EC1B9C546}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{2982C147-9446-47FE-862E-E689B64CC7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{2982C147-9446-47FE-862E-E689B64CC7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{2982C147-9446-47FE-862E-E689B64CC7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{2982C147-9446-47FE-862E-E689B64CC7E7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{006CF27E-5400-43E9-B511-C54EC1B9C546}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{006CF27E-5400-43E9-B511-C54EC1B9C546}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{006CF27E-5400-43E9-B511-C54EC1B9C546}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{006CF27E-5400-43E9-B511-C54EC1B9C546}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {2C604707-2EA1-4CCF-A89C-22B613052C8D}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "samples/Eureka/README.md",
    "content": "#Example how to use Eureka service discovery\n\nI created this becasue users are having trouble getting Eureka to work with Ocelot, hopefully this helps. \nPlease review the implementation of the individual servics to understand how everything fits together.\n\n##Instructions\n\n1. Get Eureka installed and running...\n\n    ```\n    $ git clone https://github.com/spring-cloud-samples/eureka.git\n    $ cd eureka\n    $ mvnw spring-boot:run\n    ```\n    Leave the service running\n    \n2. Get Downstream service running and registered with Eureka\n\n    ```\n    cd ./DownstreamService/\n    dotnet run\n    ```\n\n    Leave the service running\n\n3. Get API Gateway running and collecting services from Eureka\n\n    ```\n    cd ./ApiGateway/\n    dotnet run\n    ```\n\n    Leave the service running\n\n4. Make a http request to http://localhost:5000/category you should get the following response\n\n    ```json\n    [\"category1\",\"category2\"]\n    ```"
  },
  {
    "path": "samples/GraphQL/GraphQlDelegatingHandler.cs",
    "content": "﻿using GraphQL;\nusing GraphQL.NewtonsoftJson;\nusing System.Net;\nusing System.Net.Http.Headers;\n\nnamespace Ocelot.Samples.GraphQL;\n\npublic class GraphQLDelegatingHandler : DelegatingHandler\n{\n    //private readonly ISchema _schema;\n    private readonly IDocumentExecuter _executer;\n    private readonly IGraphQLTextSerializer _serializer;\n\n    public GraphQLDelegatingHandler(IDocumentExecuter executer, IGraphQLTextSerializer serializer)\n    {\n        _executer = executer;\n        _serializer = serializer;\n    }\n\n    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        //try get query from body, could check http method :)\n        var query = await request.Content!.ReadAsStringAsync(cancellationToken);\n\n        //if not body try query string, dont hack like this in real world..\n        if (query.Length == 0)\n        {\n            var decoded = WebUtility.UrlDecode(request.RequestUri!.Query);\n            query = decoded.Replace(\"?query=\", string.Empty);\n        }\n\n        var result = await _executer.ExecuteAsync(_ =>\n        {\n            _.Query = query;\n        });\n\n        // IGraphQLSerializer & IGraphQLTextSerializer: https://github.com/graphql-dotnet/graphql-dotnet/blob/master/docs2/site/docs/getting-started/transport.md#igraphqlserializer--igraphqltextserializer\n        var responseBody = _serializer.Serialize(result);\n        var media = new MediaTypeHeaderValue(\"application/graphql-response+json\");\n        var response = new HttpResponseMessage(HttpStatusCode.OK)\n        {\n            Content = new StringContent(responseBody, media),\n        };\n\n        //ocelot will treat this like any other http request...\n        return response;\n    }\n}\n"
  },
  {
    "path": "samples/GraphQL/Models/Hero.cs",
    "content": "﻿namespace Ocelot.Samples.GraphQL.Models;\n\npublic class Hero\n{\n    public int Id { get; set; }\n    public required string Name { get; set; }\n}\n"
  },
  {
    "path": "samples/GraphQL/Models/Query.cs",
    "content": "﻿using GraphQL;\n\nnamespace Ocelot.Samples.GraphQL.Models;\n\npublic class Query\n{\n    private readonly List<Hero> _heroes = new()\n    {\n        new Hero { Id = 1, Name = \"R2-D2\" },\n        new Hero { Id = 2, Name = \"Batman\" },\n        new Hero { Id = 3, Name = \"Wonder Woman\" },\n        new Hero { Id = 4, Name = \"Tom Pallister\" }\n    };\n\n    [GraphQLMetadata(\"hero\")]\n    public Hero? GetHero(int id)\n    {\n        return _heroes.FirstOrDefault(x => x.Id == id);\n    }\n}\n"
  },
  {
    "path": "samples/GraphQL/Ocelot.Samples.GraphQL.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/GraphQL</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryType>https://github.com/ThreeMammals/Ocelot.git</RepositoryType>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Update=\"README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"GraphQL\" Version=\"8.8.4\" />\n    <PackageReference Include=\"GraphQL.NewtonsoftJson\" Version=\"8.8.4\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/GraphQL/OcelotGraphQL.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.28803.452\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"OcelotGraphQL\", \"OcelotGraphQL.csproj\", \"{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {5762C36C-EA4B-44D9-9DAA-2F7C3FC98692}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "samples/GraphQL/Program.cs",
    "content": "﻿using GraphQL.Types;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Samples.GraphQL;\nusing Ocelot.Samples.GraphQL.Models;\nusing Ocelot.Samples.Web;\n\nvar schema = Schema.For(@\"\n        type Hero {\n            id: Int\n            name: String\n        }\n\n        type Query {\n            hero(id: Int): Hero\n        }\n    \", _ =>\n{\n    _.Types.Include<Query>();\n});\n\n//_ = OcelotHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\n\n// Ocelot Basic setup\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\nbuilder.Services\n    .AddSingleton<ISchema>(schema)\n    .AddOcelot(builder.Configuration)\n    .AddDelegatingHandler<GraphQLDelegatingHandler>();\n\nbuilder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\"));\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nawait app.UseOcelot();\napp.Run();\n"
  },
  {
    "path": "samples/GraphQL/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"graphql\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5559\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"graphql\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7781;http://localhost:5559\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"graphql\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7781/graphql\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7781;http://localhost:5559\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62559/\",\n      \"sslPort\": 44359\n    }\n  }\n}\n"
  },
  {
    "path": "samples/GraphQL/README.md",
    "content": "# Ocelot using GraphQL example\n\nLoads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. \nI would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would \nbring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. \n\n## Example\n\nIf you run this project with\n\n$ dotnet run\n\nUse postman or something to make the following requests and you can see Ocelot and GraphQL in action together...\n\nGET http://localhost:5000/graphql?query={ hero(id: 4) { id name } }\n\nRESPONSE\n```json\n    {\n        \"data\": {\n            \"hero\": {\n            \"id\": 4,\n            \"name\": \"Tom Pallister\"\n            }\n        }\n    }\n```\n\nPOST http://localhost:5000/graphql\n\nBODY\n```json\n    { hero(id: 4) { id name } }\n```\n\nRESPONSE\n```json\n    {\n        \"data\": {\n            \"hero\": {\n            \"id\": 4,\n            \"name\": \"Tom Pallister\"\n            }\n        }\n    }\n```\n\n## Notes\n\nPlease note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g.\n\n```json\n{\n    \"Routes\": [\n        {\n            \"DownstreamPathTemplate\": \"/graphql\",\n            \"DownstreamScheme\": \"http\",\n            \"DownstreamHostAndPorts\": [\n              {\n                \"Host\": \"yourgraphqlhost.com\",\n                \"Port\": 80\n              }\n            ],\n            \"UpstreamPathTemplate\": \"/graphql\",\n            \"DelegatingHandlers\": [\n                \"GraphQlDelegatingHandler\"\n            ]\n        }\n    ]\n  }\n```"
  },
  {
    "path": "samples/GraphQL/ocelot.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamPathTemplate\": \"/graphql\",\n      \"DownstreamPathTemplate\": \"/\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        { \"Host\": \"jsonplaceholder.typicode.com\", \"Port\": 80 }\n      ],\n      \"DelegatingHandlers\": [ \"GraphQLDelegatingHandler\" ]\n    }\n  ]\n}\n"
  },
  {
    "path": "samples/GraphQL/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"GraphQL\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==\",\n        \"dependencies\": {\n          \"GraphQL-Parser\": \"9.5.0\",\n          \"GraphQL.Analyzers\": \"8.8.4\"\n        }\n      },\n      \"GraphQL.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==\",\n        \"dependencies\": {\n          \"GraphQL\": \"[8.8.4, 9.0.0)\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"GraphQL-Parser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.5.0\",\n        \"contentHash\": \"5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==\"\n      },\n      \"GraphQL.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"GraphQL\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==\",\n        \"dependencies\": {\n          \"GraphQL-Parser\": \"9.5.0\",\n          \"GraphQL.Analyzers\": \"8.8.4\"\n        }\n      },\n      \"GraphQL.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==\",\n        \"dependencies\": {\n          \"GraphQL\": \"[8.8.4, 9.0.0)\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"GraphQL-Parser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.5.0\",\n        \"contentHash\": \"5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==\"\n      },\n      \"GraphQL.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"GraphQL\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==\",\n        \"dependencies\": {\n          \"GraphQL-Parser\": \"9.5.0\",\n          \"GraphQL.Analyzers\": \"8.8.4\"\n        }\n      },\n      \"GraphQL.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.8.4, )\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==\",\n        \"dependencies\": {\n          \"GraphQL\": \"[8.8.4, 9.0.0)\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"GraphQL-Parser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.5.0\",\n        \"contentHash\": \"5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==\"\n      },\n      \"GraphQL.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.8.4\",\n        \"contentHash\": \"uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Kubernetes/.dockerignore",
    "content": ".dockerignore\n.env\n.git\n.gitignore\n.vs\n.vscode\n*/bin\n*/obj\n**/.toolstarget"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/Dockerfile",
    "content": "FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:9.0 AS build\nWORKDIR /src\nCOPY [\"ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj\", \"ApiGateway/\"]\nRUN dotnet restore \"ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj\"\nCOPY . .\nWORKDIR \"/src/ApiGateway\"\nRUN dotnet build \"Ocelot.Samples.OcelotKube.ApiGateway.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"Ocelot.Samples.OcelotKube.ApiGateway.csproj\" -c Release -o /app/publish /p:UseAppHost=false\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"Ocelot.Samples.OcelotKube.ApiGateway.dll\"]\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Kubernetes</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot.Provider.Kubernetes\\Ocelot.Provider.Kubernetes.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/Program.cs",
    "content": "﻿using KubeClient;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Samples.Web;\n\n//_ = OcelotHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\n\ngoto Case5; // Your case should be selected here!!!\n\n// Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-bool-method\nCase1: // Use a pod service account\nbuilder.Services\n    .AddOcelot(builder.Configuration)\n    .AddKubernetes();\ngoto Start;\n\n// Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-bool-method\nCase2: // Don't use a pod service account, manually bind options\nAction<KubeClientOptions> configureOptions = opts =>\n{\n    opts.ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, \"my-host\", 443).Uri;\n    opts.AccessToken = \"my-token\";\n    opts.AuthStrategy = KubeAuthStrategy.BearerToken;\n    opts.AllowInsecure = true;\n};\nbuilder.Services\n    .AddOptions<KubeClientOptions>()\n    .Configure(configureOptions); // mannual binding options via IOptions<KubeClientOptions>\nbuilder.Services\n    .AddOcelot().AddKubernetes(false); // don't use pod service account\ngoto Start;\n\n// Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-action-kubeclientoptions-method\nCase3: // Don't use a pod service account, manually bind options, ignore global ServiceDiscoveryProvider json-options\nAction<KubeClientOptions> myOptions = opts =>\n{\n    opts.ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, \"my-host\", 443).Uri;\n    opts.AccessToken = \"my-token\";\n    opts.AuthStrategy = KubeAuthStrategy.BearerToken;\n    opts.AllowInsecure = true; // here is wrong value!\n};\nbuilder.Services\n    .AddOcelot(builder.Configuration)\n    .AddKubernetes(myOptions); // configure options with action, without optional args\ngoto Start;\n\nCase4: // Don't use a pod service account, manually bind options, ignore global ServiceDiscoveryProvider json-options\nbuilder.Services\n    .AddKubeClientOptions(opts =>\n    {\n        opts.ApiEndPoint = new UriBuilder(\"https\", \"my-host\", 443).Uri;\n        opts.AuthStrategy = KubeAuthStrategy.BearerToken;\n        opts.AccessToken = \"my-token\";\n        opts.AllowInsecure = true;\n    })\n    .AddOcelot(builder.Configuration)\n    .AddKubernetes(false); // don't use pod service account, and client options provided via AddKubeClientOptions\ngoto Start;\n\n// Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-action-kubeclientoptions-method\nCase5: // Use global ServiceDiscoveryProvider json-options\nAction<KubeClientOptions>? none = null;\nbuilder.Services\n    .AddOcelot(builder.Configuration)\n    .AddKubernetes(null, allowInsecure: true /*optional args*/) // shorten version\n    // or\n    .AddKubernetes(none, allowInsecure: true /*optional args*/) // shorten version 2\n    // or\n    .AddKubernetes(configureOptions: null, allowInsecure: true /*optional args*/); // don't configure options with action, but do with optional args\n\nStart:\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nif (app.Environment.IsDevelopment())\n{\n    app.UseDeveloperExceptionPage();\n}\nawait app.UseOcelot();\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"values\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5560\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"values\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7782;http://localhost:5560\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"values\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7782/values\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7782;http://localhost:5560\"\n      },\n      \"distributionName\": \"\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://localhost:{ServicePort}/values\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62560/\",\n      \"sslPort\": 44360\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/ocelot.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"ServiceName\": \"my-service\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/values\",\n      \"DownstreamPathTemplate\": \"/api/values\",\n      \"DownstreamScheme\": \"http\"\n    },\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/nodiscovery/values\",\n      \"DownstreamPathTemplate\": \"/api/values\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        { \"Host\": \"localhost\", \"Port\": 5561 }\n      ]\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"ServiceDiscoveryProvider\": {\n      \"Scheme\": \"https\",\n      \"Host\": \"192.168.0.13\",\n      \"Port\": 443,\n      \"Token\": \"txpc696iUhbVoudg164r93CxDTrKRVWG\",\n      \"Namespace\": \"dev\",\n      \"Type\": \"Kube\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Kubernetes/ApiGateway/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\"\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\"\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\"\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Kubernetes/Dockerfile",
    "content": "FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base\nWORKDIR /app\nEXPOSE 80\n\nFROM microsoft/dotnet:2.1-sdk AS build\nWORKDIR /src\nCOPY [\"ApiGateway/ApiGateway.csproj\", \"ApiGateway/\"]\nCOPY [\"../../src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj\", \"../../src/Ocelot.Provider.Polly/\"]\nCOPY [\"../../src/Ocelot/Ocelot.csproj\", \"../../src/Ocelot/\"]\nCOPY [\"../../src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj\", \"../../src/Ocelot.Provider.Kubernetes/\"]\nRUN dotnet restore \"ApiGateway/ApiGateway.csproj\"\nCOPY . .\nWORKDIR \"/src/ApiGateway\"\nRUN dotnet build \"ApiGateway.csproj\" -c Release -o /app\n\nFROM build AS publish\nRUN dotnet publish \"ApiGateway.csproj\" -c Release -o /app\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app .\nENTRYPOINT [\"dotnet\", \"ApiGateway.dll\"]\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc;\n\nnamespace Ocelot.Samples.Kubernetes.DownstreamService.Controllers;\n\n[ApiController]\n[Route(\"api/[controller]\")]\npublic class ValuesController : ControllerBase\n{\n    // GET api/values\n    [HttpGet]\n    public ActionResult<IEnumerable<string>> Get()\n    {\n        return new[] { \"value1\", \"value2\" };\n    }\n\n    // GET api/values/5\n    [HttpGet(\"{id}\")]\n    public ActionResult<string> Get(int id)\n    {\n        return \"value\";\n    }\n\n    // POST api/values\n    [HttpPost]\n    public void Post([FromBody] string value)\n    {\n    }\n\n    // PUT api/values/5\n    [HttpPut(\"{id}\")]\n    public void Put(int id, [FromBody] string value)\n    {\n    }\n\n    // DELETE api/values/5\n    [HttpDelete(\"{id}\")]\n    public void Delete(int id)\n    {\n    }\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc;\n\nnamespace Ocelot.Samples.Kubernetes.DownstreamService.Controllers;\n\nusing Models;\n\n[ApiController]\n[Route(\"api/[controller]\")]\npublic class WeatherForecastController : ControllerBase\n{\n    private static readonly string[] Summaries = new[]\n    {\n        \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\"\n    };\n\n    private readonly ILogger<WeatherForecastController> _logger;\n\n    public WeatherForecastController(ILogger<WeatherForecastController> logger)\n    {\n        _logger = logger;\n    }\n\n    [HttpGet(Name = \"GetWeatherForecast\")]\n    public IEnumerable<WeatherForecast> Get()\n    {\n        return Enumerable.Range(1, 5)\n            .Select(index => new WeatherForecast\n            {\n                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),\n                TemperatureC = Random.Shared.Next(-20, 55),\n                Summary = Summaries[Random.Shared.Next(Summaries.Length)]\n            })\n            .ToArray();\n    }\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Dockerfile",
    "content": "FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:9.0 AS build\nWORKDIR /src\nCOPY [\"DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj\", \"DownstreamService/\"]\nRUN dotnet restore \"DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj\"\nCOPY . .\nWORKDIR \"/src/DownstreamService\"\nRUN dotnet build \"Ocelot.Samples.OcelotKube.DownstreamService.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"Ocelot.Samples.OcelotKube.DownstreamService.csproj\" -c Release -o /app/publish /p:UseAppHost=false\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"Ocelot.Samples.OcelotKube.DownstreamService.dll\"]\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs",
    "content": "﻿namespace Ocelot.Samples.Kubernetes.DownstreamService.Models;\n\npublic class WeatherForecast\n{\n    public DateOnly Date { get; set; }\n    public int TemperatureC { get; set; }\n    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);\n    public string? Summary { get; set; }\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Kubernetes</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Swashbuckle.AspNetCore\" Version=\"10.1.5\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Program.cs",
    "content": "﻿using Ocelot.Samples.Web;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\n_ = DownstreamHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services\n    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle\n    .AddEndpointsApiExplorer()\n    .AddSwaggerGen()\n\n    .AddHttpClient()\n    .AddControllers()\n    .AddJsonOptions(options =>\n    {\n        options.AllowInputFormatterExceptionMessages = true;\n\n        var jOptions = options.JsonSerializerOptions;\n        jOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));\n        jOptions.PropertyNameCaseInsensitive = true;\n        jOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;\n    });\n\nvar app = builder.Build();\nif (app.Environment.IsDevelopment())\n{\n    app.UseSwagger();\n    app.UseSwaggerUI();\n}\napp.UseHttpsRedirection();\napp.UseAuthorization();\napp.MapControllers();\napp.Run();\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5561\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7783;http://localhost:5561\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7783/swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7783;http://localhost:5561\"\n      },\n      \"distributionName\": \"\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://localhost:{ServicePort}/swagger\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62561/\",\n      \"sslPort\": 44361\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/Kubernetes/DownstreamService/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"10.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"8.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"jDM3a95WerM8g6IcMiBXq1qRS9dqmEUpgnCk2DeMWpPkYtp1ia+CkXabOnK93JmhVlUmv8l9WMPsCSUm+WqkIA==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"9.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.0\",\n        \"contentHash\": \"1Kzzf7pRey40VaUkHN9/uWxrKVkLu2AQjt+GVeeKLLpiEHAJ1xZRsLSh4ZZYEnyS7Kt2OBOPmsXNdU+wbcOl5w==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/Kubernetes/OcelotKube.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.28803.202\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ApiGateway\", \"ApiGateway\\ApiGateway.csproj\", \"{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DownstreamService\", \"DownstreamService\\DownstreamService.csproj\", \"{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {D29790E8-4BA9-4E60-8D7D-327E21320CC9}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "samples/Metadata/API.http",
    "content": "@HostHttp = http://localhost:5139\n\nGET {{HostAddress}}/ocelot/docs/\nAccept: application/json\n### Route #1 aka 'ocelot-docs'\n\nGET {{HostAddress}}/weather/current/London\nAccept: application/json\n### route #3 aka 'weather-current'\n\nGET {{HostAddress}}/ocelot/posts/3\nAccept: application/json\n### Route #4 aka 'ocelot-posts'\n\nGET {{HostAddress}}/test/deflate\nAccept: application/json\n### Route #5\n\nGET {{HostAddress}}/test/gzip\nAccept: application/json\n### Route #6\n\n##############################################\n@HostHttps = https://localhost:7252\n\nGET {{HostHttps}}/ocelot/docs/\nAccept: application/json\n### Route #1 aka 'ocelot-docs'\n\nGET {{HostHttps}}/weather/current/London\nAccept: application/json\n### route #3 aka 'weather-current'\n\nGET {{HostHttps}}/ocelot/posts/3\nAccept: application/json\n### Route #4 aka 'ocelot-posts'\n\nGET {{HostHttps}}/test/deflate\nAccept: application/json\n### Route #5\n\nGET {{HostHttps}}/test/gzip\nAccept: application/json\n### Route #6\n"
  },
  {
    "path": "samples/Metadata/MetadataResponder.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Headers;\nusing Ocelot.Metadata;\nusing Ocelot.Middleware;\nusing Ocelot.Responder;\nusing System.IO.Compression;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing ZstdNet;\n\nnamespace Ocelot.Samples.Metadata;\n\npublic class MetadataResponder : HttpContextResponder\n{\n    public MetadataResponder(IRemoveOutputHeaders removeOutputHeaders)\n        : base(removeOutputHeaders) { }\n\n    protected override async Task WriteToUpstreamAsync(HttpContext context, DownstreamResponse downstream)\n    {\n        // Ensure the route has metadata at all\n        var route = context.Items.DownstreamRoute();\n        var metadata = route?.MetadataOptions.Metadata;\n        if ((metadata?.Count ?? 0) == 0)\n        {\n            await base.WriteToUpstreamAsync(context, downstream);\n            return;\n        }\n\n        // Content type is 'application/json', so embed route metadata JSON-node\n        var response = context.Items.DownstreamResponse();\n        if (response.Content.Headers.ContentType?.MediaType == \"application/json\")\n        {\n            // Don't process json requested by scripts aka XHR\n            if (route.GetMetadata(\"disableMetadataJson\", false))\n            {\n                AddMetadataHeader(context, metadata!); // but return in the header\n                await base.WriteToUpstreamAsync(context, downstream);\n                return;\n            }\n\n            //var json = await response.Content.ReadAsStringAsync(context.RequestAborted);\n            var json = await ReadCompressedJsonAsync(context.Response, response.Content, context.RequestAborted);\n            if (string.IsNullOrEmpty(json))\n            {\n                // Impossible to decompress content and write to body\n                // Write metadata to the contentEncoding and write original content\n                AddMetadataHeader(context, metadata!);\n                await base.WriteToUpstreamAsync(context, downstream);\n                return;\n            }\n\n            //var json1 = await JsonNode.ParseAsync(jsonStream, cancellationToken: context.RequestAborted);\n            var json1 = JsonObject.Parse(json);\n            var json2 = JsonSerializer.SerializeToNode(metadata);\n            var aggregated = new JsonObject\n            {\n                [nameof(HttpContext.Response)] = json1,\n                [nameof(MetadataOptions.Metadata)] = json2,\n            };\n            AddMetadataHeader(context, metadata!);\n            await WriteJsonAsync(context.Response, response.Content, aggregated, context.RequestAborted);\n        }\n        else\n        {\n            AddMetadataHeader(context, metadata!);\n            await base.WriteToUpstreamAsync(context, downstream);\n        }\n    }\n\n    private static void AddMetadataHeader(HttpContext context, IDictionary<string, string> metadata)\n    {\n        var node = JsonSerializer.SerializeToNode(metadata);\n        var header = node?.ToJsonString(JsonSerializerOptions.Default/*Web*/) ?? string.Empty;\n        context.Response.Headers.Append(\"OC-Route-Metadata\", new(header));\n    }\n\n    private static Encoding DetectEncoding(HttpContent content)\n    {\n        if (!string.IsNullOrEmpty(content.Headers.ContentType?.CharSet))\n        {\n            try\n            {\n                return Encoding.GetEncoding(content.Headers.ContentType.CharSet);\n            }\n            catch (ArgumentException)\n            {\n                return Encoding.UTF8; // unknown encoding, fallback to UTF-8\n            }\n        }\n        return Encoding.UTF8; // default to UTF-8\n    }\n\n    private static async Task<string> ReadCompressedJsonAsync(HttpResponse response, HttpContent content, CancellationToken token)\n    {\n        var encoding = DetectEncoding(content);\n        if (content.Headers.ContentEncoding.Contains(\"br\")) // Brotli compression: https://www.prowaretech.com/articles/current/dot-net/compression-brotli\n        {\n            using var compressed = await content.ReadAsStreamAsync(token);\n            var decompressed = await DecompressBrotliAsync(compressed, token);\n            return encoding.GetString(decompressed);\n        }\n        else if (content.Headers.ContentEncoding.Contains(\"zstd\")) // Zstandard compression: https://github.com/facebook/zstd\n        {\n            using var compressed = await content.ReadAsStreamAsync(token);\n            var decompressed = await DecompressZstandardAsync(compressed, token);\n            return encoding.GetString(decompressed);\n        }\n        else if (content.Headers.ContentEncoding.Contains(\"deflate\")) // Deflate algorithm compression\n        {\n            // Actually it doesn't work: only MS compressed DeflateStream are supported\n            // Decompressor will generate System.IO.InvalidDataException: The archive entry was compressed using an unsupported compression method.\n            //var compressed = await content.ReadAsStreamAsync(token);\n            //var decompressed = await DecompressDeflateAsync(compressed, token);\n            //return encoding.GetString(decompressed);\n            return string.Empty;\n        }\n        else if (content.Headers.ContentEncoding.Contains(\"gzip\")) // GZip compression\n        {\n            var compressed = await content.ReadAsStreamAsync(token);\n            var decompressed = DecompressGZip(compressed);\n            return encoding.GetString(decompressed);\n        }\n        else // no compression\n        {\n            var buffer = await content.ReadAsByteArrayAsync(token);\n            return encoding.GetString(buffer);\n        }\n    }\n\n    private static Task WriteJsonAsync(HttpResponse to, HttpContent content, JsonObject json, CancellationToken token)\n    {\n        // We will not use original downstrean encoding, so defaults always to UTF8 for upstream\n        var encoding = Encoding.UTF8; // DetectEncoding(content);\n        var serialized = json.ToJsonString(JsonSerializerOptions.Default/*Web*/); // will not require compression\n        var buffer = encoding.GetBytes(serialized);\n        to.ContentLength = buffer.Length; // don't use chunked\n\n        var ct = content.Headers.ContentType ?? new(\"application/json\", encoding.HeaderName);\n        ct.CharSet = encoding.HeaderName;\n        to.ContentType = ct.ToString(); // always -> application/json; charset=utf-8\n\n        var contentEncoding = content.Headers.ContentEncoding;\n        if (contentEncoding.Contains(\"br\")) // Brotli compression\n        {\n            //var compressed = await CompressBrotliAsync(buffer, token);\n            //var data = encoding.GetString(compressed);\n            //await to.WriteAsync(data, encoding, token); // client app error: Decompression failed\n            to.Headers.ContentEncoding = new(\"identity\"); // don't compress with Brotli algo\n        }\n        else if (contentEncoding.Contains(\"zstd\")) // Zstandard compression (Facebook)\n        {\n            to.Headers.ContentEncoding = new(\"identity\"); // don't compress with Zstandard algo\n        }\n        else if (contentEncoding.Contains(\"deflate\")) // Deflate compression\n        {\n            // Do nothing, because of impossibility to decompress third-party streams\n        }\n        else if (contentEncoding.Contains(\"gzip\")) // GZip compression\n        {\n            to.Headers.ContentEncoding = new(\"identity\"); // don't compress with GZip algo\n        }\n        return to.Body.WriteAsync(buffer, 0, buffer.Length, token);\n    }\n\n    // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.brotlistream?view=net-9.0\n    private static async Task<byte[]> CompressBrotliAsync(byte[] input, CancellationToken token)\n    {\n        using var output = new MemoryStream(input.Length);\n        using var brotli = new BrotliStream(output, CompressionLevel.SmallestSize);\n        await brotli.WriteAsync(input, token);\n        await brotli.FlushAsync(token);\n        return output.ToArray();\n    }\n\n    // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.brotlistream?view=net-9.0\n    private static async Task<byte[]> DecompressBrotliAsync(Stream input, CancellationToken token)\n    {\n        using var output = new MemoryStream();\n        using var decompressor = new BrotliStream(input, CompressionMode.Decompress);\n        await decompressor.CopyToAsync(output, token);\n        return output.ToArray();\n    }\n\n    // https://github.com/skbkontur/ZstdNet\n    private static async Task<byte[]> DecompressZstandardAsync(Stream input, CancellationToken token)\n    {\n        using var output = new MemoryStream();\n        await using var decompression = new DecompressionStream(input);\n        await decompression.CopyToAsync(output, token);\n        return output.ToArray();\n    }\n\n    // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream?view=net-9.0\n    private static async Task<byte[]> DecompressDeflateAsync(Stream input, CancellationToken token)\n    {\n        using var output = new MemoryStream();\n        using var decompressor = new DeflateStream(input, CompressionMode.Decompress);\n        await decompressor.CopyToAsync(output, token);\n        return output.ToArray();\n    }\n\n    // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream?view=net-9.0\n    public static byte[] DecompressGZip(Stream input)\n    {\n        using var output = new MemoryStream();\n        using var decompressor = new GZipStream(input, CompressionMode.Decompress);\n        decompressor.CopyTo(output);\n        return output.ToArray();\n    }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/PostsPlugin2.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class PostsPlugin2\n{\n    public string? name { get; set; }\n    public int? age { get; set; }\n    public string? city { get; set; }\n    public bool? is_student { get; set; }\n    public string[]? hobbies { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/TestDeflateResponse.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class TestDeflateResponse\n{\n    public bool? deflated { get; set; }\n    public Dictionary<string, string>? headers { get; set; }\n    public string? method { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/TestGZipResponse.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class TestGZipResponse\n{\n    public bool? gzipped { get; set; }\n    public Dictionary<string, string>? headers { get; set; }\n    public string? method { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/WeatherCurrent.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class WeatherCurrent\n{\n    public long? last_updated_epoch { get; set; }\n    public string? last_updated { get; set; }\n    public float? temp_c { get; set; }\n    public float? temp_f { get; set; }\n    public int? is_day { get; set; }\n    public WeatherCurrentCondition? condition { get; set; }\n    public float? wind_mph { get; set; }\n    public float? wind_kph { get; set; }\n    public int? wind_degree { get; set; }\n    public string? wind_dir { get; set; }\n    public float? pressure_mb { get; set; }\n    public float? pressure_in { get; set; }\n    public float? precip_mm { get; set; }\n    public float? precip_in { get; set; }\n    public int? humidity { get; set; }\n    public int? cloud { get; set; }\n    public float? feelslike_c { get; set; }\n    public float? feelslike_f { get; set; }\n    public float? windchill_c { get; set; }\n    public float? windchill_f { get; set; }\n    public float? heatindex_c { get; set; }\n    public float? heatindex_f { get; set; }\n    public float? dewpoint_c { get; set; }\n    public float? dewpoint_f { get; set; }\n    public float? vis_km { get; set; }\n    public float? vis_miles { get; set; }\n    public float? uv { get; set; }\n    public float? gust_mph { get; set; }\n    public float? gust_kph { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/WeatherCurrentCondition.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class WeatherCurrentCondition\n{\n    public string? text { get; set; }\n    public string? icon { get; set; }\n    public int? code { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/WeatherLocation.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class WeatherLocation\n{\n    public string? name { get; set; }\n    public string? region { get; set; }\n    public string? country { get; set; }\n    public float? lat { get; set; }\n    public float? lon { get; set; }\n    public string? tz_id { get; set; }\n    public long? localtime_epoch { get; set; }\n    public string? localtime { get; set; }\n}\n"
  },
  {
    "path": "samples/Metadata/Models/WeatherResponse.cs",
    "content": "﻿namespace Ocelot.Samples.Metadata.Models;\n\npublic class WeatherResponse\n{\n    public WeatherLocation? location { get; set; }\n    public WeatherCurrent? current { get; set; }\n\n}\n"
  },
  {
    "path": "samples/Metadata/MyMiddlewares.cs",
    "content": "using Ocelot.Logging;\nusing Ocelot.Metadata;\nusing Ocelot.Middleware;\nusing Ocelot.Responder;\nusing System.Text.Json;\n\nnamespace Ocelot.Samples.Metadata;\nusing Models;\n\nclass MyMiddlewares\n{\n    public static async Task PreErrorResponderMiddleware(HttpContext context, Func<Task> next)\n    {\n        // Get downstream response first\n        await next.Invoke(); // ResponderMiddleware\n\n        var loggerFactory = context.RequestServices.GetRequiredService<IOcelotLoggerFactory>();\n        var logger = loggerFactory.CreateLogger<MyMiddlewares>();\n        logger.LogDebug(() => $\"My custom {nameof(PreErrorResponderMiddleware)} started\");\n\n        var route = context.Items.DownstreamRoute();\n        var routeId = route.GetMetadata<int>(\"route.id\");\n        var routeName = route.GetMetadata(\"route.name\", string.Empty);\n        switch (routeId)\n        {\n            case 1: // ocelot-docs\n            case 2: // ocelot-docs-BFF\n                bool disabled = route.GetMetadata<bool>(\"disableMetadataJson\");\n                break;\n            case 3: // weather-current\n                var cities = route.GetMetadata<string[]>(\"cities\");\n                var defaultCity = route.GetMetadata<string>(\"cities.default\");\n                var citiesUS = route.GetMetadata<string[]>(\"cities.US\");\n                var pathTemperatureCelsius = route.GetMetadata<string>(\"temperature-celsius-path\");\n                var dataResponse = route.GetMetadata<WeatherResponse>(\"data/Response\", new());\n                // TODO Refactor Ocelot Metadata helpers and Ocelot Core to support propagation of the JsonElement and JsonNode\n                //var dataLocation = route.GetMetadataElement(\"stub-data/location\", new JsonElement());\n                break;\n            case 4: // ocelot-posts\n                var id = route.GetMetadata<string>(\"id\");\n                var tags = route.GetMetadata<string[]>(\"tags\");\n\n                // Plugin 1 data\n                var p1Enabled = route.GetMetadata<bool>(\"plugin1.enabled\");\n                var p1Values = route.GetMetadata<string[]>(\"plugin1.values\");\n                var p1Param = route.GetMetadata(\"plugin1.param\", \"system-default-value\");\n                var p1Param2 = route.GetMetadata<int>(\"plugin1.param2\");\n\n                // Plugin 2 data\n                var p2Param1 = route.GetMetadata(\"plugin2/param1\", \"default-value\");\n                var plugin2 = route.GetMetadata<PostsPlugin2>(\"plugin2/data\", new());\n                break;\n            case 5: // test-deflate\n                var response1 = route.GetMetadata<TestDeflateResponse>(\"data/Response\", new());\n                break;\n            case 6: // test-gzip\n                var json = route.GetMetadata(\"data/Response\", \"{}\"); // parse data manually\n                var response2 = JsonSerializer.Deserialize<TestGZipResponse>(json);\n                break;\n        }\n        // Reading global metadata\n        var globalAppName = route.GetMetadata<string>(\"app-name\");\n        // Working with metadata\n        // ...\n    }\n\n    public static async Task ResponderMiddleware(HttpContext context, Func<Task> next)\n    {\n        // Prepare services\n        var responder = context.RequestServices.GetRequiredService<IHttpResponder>();\n        var loggerFactory = context.RequestServices.GetRequiredService<IOcelotLoggerFactory>();\n        var logger = loggerFactory.CreateLogger<MyMiddlewares>();\n        var codeMapper = context.RequestServices.GetRequiredService<IErrorsToHttpStatusCodeMapper>();\n        logger.LogDebug(() => $\"My custom {nameof(ResponderMiddleware)} started\");\n\n        // Call original middleware\n        Task @delegate(HttpContext c) => next();\n        var @base = new Responder.Middleware.ResponderMiddleware(@delegate, responder, loggerFactory, codeMapper);\n        await @base.Invoke(context); // next.Invoke()\n    }\n}\n"
  },
  {
    "path": "samples/Metadata/Ocelot.Samples.Metadata.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Metadata</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"ZstdNet\" Version=\"1.5.7\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/Metadata/Program.cs",
    "content": "using Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Responder;\nusing Ocelot.Samples.Metadata;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\nbuilder.Services\n    .AddOcelot(builder.Configuration)\n    .Services.RemoveAll<IHttpResponder>()\n    .TryAddSingleton<IHttpResponder, MetadataResponder>();\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nvar configuration = new OcelotPipelineConfiguration\n{\n    PreErrorResponderMiddleware = MyMiddlewares.PreErrorResponderMiddleware,\n    ResponderMiddleware = MyMiddlewares.ResponderMiddleware, // can be switched off/on\n};\nawait app.UseOcelot(configuration);\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/Metadata/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": false,\n      \"applicationUrl\": \"http://localhost:5139\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": false,\n      \"applicationUrl\": \"https://localhost:7252;http://localhost:5139\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Metadata/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Metadata/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/Metadata/ocelot.json",
    "content": "{\n  \"Routes\": [\n    { // route #1 aka 'ocelot-docs'\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/docs/{everything}\",\n      \"DownstreamPathTemplate\": \"/en/latest/{everything}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 1,\n        \"route.name\": \"ocelot-docs\",\n        \"disableMetadataJson\": true // don't process json requested by scripts aka XHR\n      }\n    },\n    { // route #2 aka 'ocelot-docs-BFF'\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamPathTemplate\": \"/_/{BFF}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"ocelot.readthedocs.io\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 2,\n        \"route.name\": \"ocelot-docs-BFF\",\n        \"disableMetadataJson\": true // don't process json requested by scripts aka XHR\n      }\n    },\n    { // route #3 aka 'weather-current'\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/weather/current/{city}\",\n      \"DownstreamPathTemplate\": \"/v1/current.json?q={city}&key=4ea9a1d2aafe4e15bbd173615242312\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"api.weatherapi.com\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 3,\n        \"route.name\": \"weather-current\",\n        \"cities\": \"London, Paris, Madrid, Berlin, Rome, Prague, Warsaw, Minsk\",\n        \"cities.default\": \"London\",\n        \"cities.US\": \"New York, Los Angeles\",\n        \"temperature-celsius-path\": \"Response/current/temp_c\",\n        \"stub-data/location\": {\n          \"name\": \"London\",\n          \"region\": \"City of London, Greater London\",\n          \"country\": \"United Kingdom\",\n          \"lat\": 51.5171,\n          \"lon\": -0.1062,\n          \"tz_id\": \"Europe/London\",\n          \"localtime_epoch\": 1741269500,\n          \"localtime\": \"2025-03-06 13:58\"\n        },\n        \"stub-data/current\": {\n          \"last_updated_epoch\": 1741268700,\n          \"last_updated\": \"2025-03-06 13:45\",\n          \"temp_c\": 17.1,\n          \"temp_f\": 62.8,\n          \"is_day\": 1,\n          \"condition\": {\n            \"text\": \"Sunny\",\n            \"icon\": \"//cdn.weatherapi.com/weather/64x64/day/113.png\",\n            \"code\": 1000\n          },\n          \"wind_mph\": 12.1,\n          \"wind_kph\": 19.4,\n          \"wind_degree\": 185,\n          \"wind_dir\": \"S\",\n          \"pressure_mb\": 1012.0,\n          \"pressure_in\": 29.88,\n          \"precip_mm\": 0.0,\n          \"precip_in\": 0.0,\n          \"humidity\": 45,\n          \"cloud\": 0,\n          \"feelslike_c\": 17.1,\n          \"feelslike_f\": 62.8,\n          \"windchill_c\": 12.8,\n          \"windchill_f\": 55.0,\n          \"heatindex_c\": 14.3,\n          \"heatindex_f\": 57.7,\n          \"dewpoint_c\": 4.4,\n          \"dewpoint_f\": 39.9,\n          \"vis_km\": 10.0,\n          \"vis_miles\": 6.0,\n          \"uv\": 2.3,\n          \"gust_mph\": 14.1,\n          \"gust_kph\": 22.7\n        },\n        \"data/Response\": \"{\\\"location\\\":{\\\"name\\\":\\\"London\\\",\\\"region\\\":\\\"City of London, Greater London\\\",\\\"country\\\":\\\"United Kingdom\\\",\\\"lat\\\":51.5171,\\\"lon\\\":-0.1062,\\\"tz_id\\\":\\\"Europe/London\\\",\\\"localtime_epoch\\\":1741269500,\\\"localtime\\\":\\\"2025-03-06 13:58\\\"},\\\"current\\\":{\\\"last_updated_epoch\\\":1741268700,\\\"last_updated\\\":\\\"2025-03-06 13:45\\\",\\\"temp_c\\\":17.1,\\\"temp_f\\\":62.8,\\\"is_day\\\":1,\\\"condition\\\":{\\\"text\\\":\\\"Sunny\\\",\\\"icon\\\":\\\"//cdn.weatherapi.com/weather/64x64/day/113.png\\\",\\\"code\\\":1000},\\\"wind_mph\\\":12.1,\\\"wind_kph\\\":19.4,\\\"wind_degree\\\":185,\\\"wind_dir\\\":\\\"S\\\",\\\"pressure_mb\\\":1012.0,\\\"pressure_in\\\":29.88,\\\"precip_mm\\\":0.0,\\\"precip_in\\\":0.0,\\\"humidity\\\":45,\\\"cloud\\\":0,\\\"feelslike_c\\\":17.1,\\\"feelslike_f\\\":62.8,\\\"windchill_c\\\":12.8,\\\"windchill_f\\\":55.0,\\\"heatindex_c\\\":14.3,\\\"heatindex_f\\\":57.7,\\\"dewpoint_c\\\":4.4,\\\"dewpoint_f\\\":39.9,\\\"vis_km\\\":10.0,\\\"vis_miles\\\":6.0,\\\"uv\\\":2.3,\\\"gust_mph\\\":14.1,\\\"gust_kph\\\":22.7}}\"\n      } // end of metadata\n    }, // end of route #3 aka 'weather-current'\n    { // route #4 aka 'ocelot-posts'\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/ocelot/posts/{id}\",\n      \"DownstreamPathTemplate\": \"/todos/{id}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 4,\n        \"route.name\": \"ocelot-posts\",\n        \"id\": \"FindPost\",\n        \"tags\": \"tag1, tag2, area1, area2, func1\",\n        \"plugin1.enabled\": \"true\",\n        \"plugin1.values\": \"[1, 2, 3, 4, 5]\",\n        \"plugin1.param\": \"value2\",\n        \"plugin1.param2\": \"123\",\n        \"plugin2/param1\": \"overwritten-value\",\n        \"plugin2/data\": \"{\\\"name\\\":\\\"John Doe\\\",\\\"age\\\":30,\\\"city\\\":\\\"New York\\\",\\\"is_student\\\":false,\\\"hobbies\\\":[\\\"reading\\\",\\\"hiking\\\",\\\"cooking\\\"]}\"\n      } // end of metadata\n    }, // end of route #4 aka 'ocelot-posts'\n    { // route #5\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/test/deflate\",\n      \"DownstreamPathTemplate\": \"/deflate\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"postman-echo.com\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 5,\n        \"route.name\": \"test-deflated\",\n        \"data/Response\": \"{\\\"deflated\\\": true,\\\"headers\\\":{\\\"host\\\":\\\"postman-echo.com\\\",\\\"x-request-start\\\":\\\"t1741445435.299\\\",\\\"connection\\\":\\\"close\\\",\\\"x-forwarded-proto\\\":\\\"https\\\",\\\"x-forwarded-port\\\":\\\"443\\\",\\\"x-amzn-trace-id\\\":\\\"Root=1-67cc593b-697296304a4cdd9f25ff5b1a\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"PostmanRuntime/7.43.0\\\",\\\"accept-encoding\\\":\\\"gzip, deflate, br\\\",\\\"cookie\\\":\\\"sails.sid=s%3Al8bqLyxyBLVEzTvXkXmqzKy-oxu-Ofix.n5%2F%2FJgIegPYVSpaNLkI3oaLMBTeCG7rSDP95Tvzexx0\\\",\\\"oc-data\\\":\\\"deflate, gzip\\\",\\\"postman-token\\\":\\\"0db2b611-6e61-4c07-80cf-72a25b5dbff9\\\",\\\"traceparent\\\":\\\"00-d293c56e9acc1b154d470acf93f605b6-677ec7e38b6a8f1f-00\\\"},\\\"method\\\":\\\"GET\\\"}\"\n      }\n    },\n    { // route #6\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/test/gzip\",\n      \"DownstreamPathTemplate\": \"/gzip\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"postman-echo.com\",\n          \"Port\": 443\n        }\n      ],\n      \"Metadata\": {\n        \"route.id\": 6,\n        \"route.name\": \"test-gzip\",\n        \"data/Response\": \"{\\\"gzipped\\\":true,\\\"headers\\\":{\\\"host\\\":\\\"postman-echo.com\\\",\\\"x-request-start\\\":\\\"t1741444484.025\\\",\\\"connection\\\":\\\"close\\\",\\\"x-forwarded-proto\\\":\\\"https\\\",\\\"x-forwarded-port\\\":\\\"443\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"PostmanRuntime/7.43.0\\\",\\\"accept-encoding\\\":\\\"gzip, deflate, br\\\",\\\"oc-data\\\":\\\"deflate, gzip\\\",\\\"postman-token\\\":\\\"50831f03-e4ed-4970-bd33-69720171d016\\\",\\\"traceparent\\\":\\\"00-6f958a4ad549dd11fc08b5a411277688-40a2fe22bf119682-00\\\"},\\\"method\\\":\\\"GET\\\"}\"\n      }\n    } // end of route #6\n  ],\n  \"GlobalConfiguration\": {\n    \"BaseUrl\": \"http://localhost:5139\",\n    \"Metadata\": {\n      \"app-name\": \"Ocelot Metadata sample\"\n    },\n    \"MetadataOptions\": {\n      \"CurrentCulture\": \"en-GB\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Metadata/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"ZstdNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.5.7, )\",\n        \"resolved\": \"1.5.7\",\n        \"contentHash\": \"BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"ZstdNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.5.7, )\",\n        \"resolved\": \"1.5.7\",\n        \"contentHash\": \"BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"ZstdNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.5.7, )\",\n        \"resolved\": \"1.5.7\",\n        \"contentHash\": \"BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>25.0.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/OpenTracing</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Jaeger\" Version=\"1.0.3\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.3\" />\n    <PackageReference Include=\"Ocelot.Tracing.OpenTracing\" Version=\"25.0.0-beta.2\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/OpenTracing/Program.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Samples.Web;\nusing Ocelot.Tracing.OpenTracing;\nusing OpenTracing.Util;\n\n//_ = OcelotHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\nbuilder.Services\n    .AddSingleton(serviceProvider =>\n    {\n        var loggerFactory = serviceProvider.GetService<ILoggerFactory>();\n        var config = new Jaeger.Configuration(builder.Environment.ApplicationName, loggerFactory);\n        var tracer = config.GetTracer();\n        GlobalTracer.Register(tracer);\n        return tracer;\n    })\n    .AddOcelot(builder.Configuration)\n    .AddOpenTracing();\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nawait app.UseOcelot();\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/OpenTracing/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5562\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7784;http://localhost:5562\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7784/posts/1\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7784;http://localhost:5562\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62562/\",\n      \"sslPort\": 44362\n    }\n  }\n}\n"
  },
  {
    "path": "samples/OpenTracing/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/OpenTracing/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/OpenTracing/ocelot.json",
    "content": "{\n  \"Routes\": [\n    {\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/posts/{id}\",\n      \"DownstreamPathTemplate\": \"/todos/{id}\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        { \"Host\": \"jsonplaceholder.typicode.com\", \"Port\": 443 }\n      ],\n      \"HttpHandlerOptions\": {\n        \"UseTracing\": true\n      }\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"BaseUrl\": \"https://localhost:7784\"\n  }\n}\n"
  },
  {
    "path": "samples/OpenTracing/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Jaeger\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.0.3, )\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==\",\n        \"dependencies\": {\n          \"Jaeger.Core\": \"1.0.3\",\n          \"Jaeger.Senders.Thrift\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.3, )\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==\"\n      },\n      \"Ocelot.Tracing.OpenTracing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[25.0.0-beta.2, )\",\n        \"resolved\": \"25.0.0-beta.2\",\n        \"contentHash\": \"C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==\",\n        \"dependencies\": {\n          \"Ocelot\": \"25.0.0-beta.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"ApacheThrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.14.1\",\n        \"contentHash\": \"WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Abstractions\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"3.1.0\",\n          \"System.Net.Http.WinHttpHandler\": \"4.7.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Jaeger.Communication.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==\",\n        \"dependencies\": {\n          \"ApacheThrift\": \"0.14.1\"\n        }\n      },\n      \"Jaeger.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Newtonsoft.Json\": \"12.0.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"Jaeger.Senders.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==\",\n        \"dependencies\": {\n          \"Jaeger.Communication.Thrift\": \"1.0.3\",\n          \"Jaeger.Core\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Features\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Features\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"IHsqsECi1N2FJ0RmV73Cmp6qusu4vGBhUuWJFyJAC/LekFdwSa5zacZE80Sd8M2fD9ZXgEaA32y5qcj3jh3wlQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"LLPdY4BEQ94be1eiXYyeFhcern4jOoMgIKLmfFpEvXafbcsSZtCXk0yT6seoyCJsh1vrdTVKYbLH+3b6/actfg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"gnCyVHEYeI3oeK1pig6F3ckmTKew5wJO5V70vj7rKp4KOoPUijGcigsaFdJfj5HZBXMmYuJpBiaWCHauXJ0GLw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==\"\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Ocelot\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"25.0.0-beta.1\",\n        \"contentHash\": \"uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==\",\n        \"dependencies\": {\n          \"FluentValidation\": \"12.1.1\",\n          \"IPAddressRange\": \"6.3.0\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"10.0.3\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"10.0.3\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"3.1.32\"\n        }\n      },\n      \"OpenTracing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.12.1\",\n        \"contentHash\": \"8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==\"\n      },\n      \"System.Net.Http.WinHttpHandler\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.7.0\",\n        \"contentHash\": \"fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Jaeger\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.0.3, )\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==\",\n        \"dependencies\": {\n          \"Jaeger.Core\": \"1.0.3\",\n          \"Jaeger.Senders.Thrift\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.3, )\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==\"\n      },\n      \"Ocelot.Tracing.OpenTracing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[25.0.0-beta.2, )\",\n        \"resolved\": \"25.0.0-beta.2\",\n        \"contentHash\": \"C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==\",\n        \"dependencies\": {\n          \"Ocelot\": \"25.0.0-beta.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"ApacheThrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.14.1\",\n        \"contentHash\": \"WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Abstractions\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"3.1.0\",\n          \"System.Net.Http.WinHttpHandler\": \"4.7.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Jaeger.Communication.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==\",\n        \"dependencies\": {\n          \"ApacheThrift\": \"0.14.1\"\n        }\n      },\n      \"Jaeger.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Newtonsoft.Json\": \"12.0.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"Jaeger.Senders.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==\",\n        \"dependencies\": {\n          \"Jaeger.Communication.Thrift\": \"1.0.3\",\n          \"Jaeger.Core\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Features\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Features\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.24\",\n        \"contentHash\": \"fup+Ya6mN58877F6eKzR8jrMe2fCRQ/Bl3pA/23DtX+1R2eWdDTrZGYOGDrnt2aWN5VgLSlxc7APFgXiK57l8w==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.24\",\n        \"contentHash\": \"qb0pE7PBNUiIVtFleAZ4gq7KLQuPGOjAhA4TbC/NLLpsP1WXJtDXcqTBdta6iJQBDtmeWVSijy6KyX0hZcr/WQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.24\",\n        \"contentHash\": \"TvbyHnoETdT71rTFlBLUJ6pOCu1nQf4Y4dkt/g2lEqKN2+CSraY2rUPyYrpPeH5oopSQGrDNFO3pVCBrfbjxjg==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.24\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==\"\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Ocelot\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"25.0.0-beta.1\",\n        \"contentHash\": \"uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==\",\n        \"dependencies\": {\n          \"FluentValidation\": \"12.1.1\",\n          \"IPAddressRange\": \"6.3.0\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"8.0.24\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"8.0.24\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"3.1.32\"\n        }\n      },\n      \"OpenTracing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.12.1\",\n        \"contentHash\": \"8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==\"\n      },\n      \"System.Net.Http.WinHttpHandler\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.7.0\",\n        \"contentHash\": \"fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Jaeger\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.0.3, )\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==\",\n        \"dependencies\": {\n          \"Jaeger.Core\": \"1.0.3\",\n          \"Jaeger.Senders.Thrift\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.3, )\",\n        \"resolved\": \"10.0.3\",\n        \"contentHash\": \"bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==\"\n      },\n      \"Ocelot.Tracing.OpenTracing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[25.0.0-beta.2, )\",\n        \"resolved\": \"25.0.0-beta.2\",\n        \"contentHash\": \"C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==\",\n        \"dependencies\": {\n          \"Ocelot\": \"25.0.0-beta.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"ApacheThrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.14.1\",\n        \"contentHash\": \"WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Abstractions\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"3.1.0\",\n          \"System.Net.Http.WinHttpHandler\": \"4.7.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Jaeger.Communication.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==\",\n        \"dependencies\": {\n          \"ApacheThrift\": \"0.14.1\"\n        }\n      },\n      \"Jaeger.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"2.2.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Newtonsoft.Json\": \"12.0.1\",\n          \"OpenTracing\": \"0.12.1\"\n        }\n      },\n      \"Jaeger.Senders.Thrift\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.3\",\n        \"contentHash\": \"3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==\",\n        \"dependencies\": {\n          \"Jaeger.Communication.Thrift\": \"1.0.3\",\n          \"Jaeger.Core\": \"1.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Http.Features\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.Http.Features\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.13\",\n        \"contentHash\": \"97bu/KDJKJypkpQb0hq2YDxFy4f30g/4Wmk2I8XTxDvaXbGL2UcLQGdrLWAIW+NlEAFI+Zrps1Oe92uO26vRLQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.13\",\n        \"contentHash\": \"qfh2o5iXQvummtKgaui21dbmOjhBoQfwscxgfxDUUlvNa+Qj6hMwqQUOLQ+/oG+8caUDkdSWzMdcu8Z79UT4GQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.13\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.13\",\n        \"contentHash\": \"hH3hfEYrm97r5+11BeezwT4LmDvgGPzq3GvtChhCV9AA2igWPkzA5E0ZmtPWdU9W124QZmceMztDZs68xgkHOw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.13\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.0\",\n        \"contentHash\": \"gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"2.2.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==\"\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Primitives\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"3.1.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Ocelot\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"25.0.0-beta.1\",\n        \"contentHash\": \"uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==\",\n        \"dependencies\": {\n          \"FluentValidation\": \"12.1.1\",\n          \"IPAddressRange\": \"6.3.0\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"9.0.13\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"9.0.13\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"3.1.32\"\n        }\n      },\n      \"OpenTracing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.12.1\",\n        \"contentHash\": \"8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==\"\n      },\n      \"System.Net.Http.WinHttpHandler\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.7.0\",\n        \"contentHash\": \"fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/ServiceDiscovery/.dockerignore",
    "content": "**/.classpath\n**/.dockerignore\n**/.env\n**/.git\n**/.gitignore\n**/.project\n**/.settings\n**/.toolstarget\n**/.vs\n**/.vscode\n**/*.*proj.user\n**/*.dbmdl\n**/*.jfm\n**/azds.yaml\n**/bin\n**/charts\n**/docker-compose*\n**/Dockerfile*\n**/node_modules\n**/npm-debug.log\n**/obj\n**/secrets.dev.yaml\n**/values.dev.yaml\nLICENSE\nREADME.md\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Leon Lucardie, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/Program.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery;\nusing Ocelot.Samples.Web;\nusing Ocelot.ServiceDiscovery;\n\n_ = OcelotHostBuilder.Create(args);\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Configuration\n    .SetBasePath(builder.Environment.ContentRootPath)\n    .AddOcelot();\n\n// Perform initialization from application configuration or hardcode/choose the best option.\nbool easyWay = true;\n\nif (easyWay)\n{\n    // Design #1: Define a custom finder delegate to instantiate a custom provider \n    // under the default factory (ServiceDiscoveryProviderFactory).\n    builder.Services\n        .AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)\n            => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute));\n}\nelse\n{\n    // Design #2: Abstract from the default factory (ServiceDiscoveryProviderFactory) and FinderDelegate,\n    // and create your own factory by implementing the IServiceDiscoveryProviderFactory interface.\n    builder.Services\n        .RemoveAll<IServiceDiscoveryProviderFactory>()\n        .AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();\n\n    // This will not be called but is required for internal validators. It's also a handy workaround.\n    builder.Services\n        .AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute) => null);\n}\n\nbuilder.Services\n    .AddOcelot(builder.Configuration);\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nawait app.UseOcelot();\nawait app.RunAsync();\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"categories\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5563\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"categories\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7785;http://localhost:5563\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"categories\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7785/categories\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7785;http://localhost:5563\"\n      },\n      \"distributionName\": \"\"\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62563/\",\n      \"sslPort\": 44363\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Metadata;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery;\n\npublic class MyServiceDiscoveryProvider : IServiceDiscoveryProvider\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly ServiceProviderConfiguration _config;\n    private readonly DownstreamRoute _downstreamRoute;\n\n    public MyServiceDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute)\n    {\n        _serviceProvider = serviceProvider;\n        _config = config;\n        _downstreamRoute = downstreamRoute;\n    }\n\n    public Task<List<Service>> GetAsync()\n    {\n\n        // Returns a list of service(s) that match the downstream route passed to the provider\n        var services = new List<Service>();\n\n        // Apply configuration checks\n        // ... if (_config.Host)\n\n        if (_downstreamRoute.ServiceName.Equals(\"downstream-service\"))\n        {\n            var instance = _downstreamRoute.GetMetadata<string>(\"instance\");\n            var srv = instance.Split(':');\n\n            //For this example we simply do a manual match to a single service\n            var service = new Service(\n                name: \"downstream-service\",\n                hostAndPort: new ServiceHostAndPort(srv[0], int.Parse(srv[1])),\n                id: \"downstream-service-1\",\n                version: \"1.0\",\n                tags: new string[] { \"downstream\", \"hardcoded\" }\n            );\n\n            services.Add(service);\n        }\n\n        return Task.FromResult(services);\n    }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery;\n\npublic class MyServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory\n{\n    private readonly IOcelotLoggerFactory _factory;\n    private readonly IServiceProvider _provider;\n\n    public MyServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider)\n    {\n        _factory = factory;\n        _provider = provider;\n    }\n\n    public Response<IServiceDiscoveryProvider> Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route)\n    {\n        // Apply configuration checks\n        // ...\n\n        // Create the provider based on configuration and route info\n        var provider = new MyServiceDiscoveryProvider(_provider, serviceConfig, route);\n\n        return new OkResponse<IServiceDiscoveryProvider>(provider);\n    }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/ocelot.json",
    "content": "﻿{\n  \"Routes\": [\n    {\n      \"ServiceName\": \"downstream-service\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/categories\",\n      \"DownstreamPathTemplate\": \"/categories\",\n      \"DownstreamScheme\": \"https\",\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 },\n      \"Metadata\": {\n        \"instance\": \"localhost:7786\"\n      }\n    },\n    {\n      \"ServiceName\": \"downstream-service\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"UpstreamPathTemplate\": \"/health\",\n      \"DownstreamPathTemplate\": \"/health\",\n      \"DownstreamScheme\": \"https\",\n      \"Metadata\": {\n        \"instance\": \"localhost:7786\"\n      }\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"RequestIdKey\": \"OcRequestId\",\n    \"AdministrationPath\": \"/administration\",\n    \"ServiceDiscoveryProvider\": {\n      \"Type\": \"MyServiceDiscoveryProvider\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/ApiGateway/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs",
    "content": "﻿namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class CategoriesController : ControllerBase\n{\n    // GET /categories\n    [HttpGet]\n    public IEnumerable<string> Get()\n    {\n        var random = new Random();\n        int max = DateTime.Now.Second;\n        int length = random.Next(max);\n        var categories = new List<string>(length);\n        for (int i = 0; i < length; i++)\n        {\n            max = DateTime.Now.Millisecond < 3\n                ? DateTime.Now.Millisecond + 3 : DateTime.Now.Millisecond;\n            categories.Add(\"category\" + random.Next(max));\n        }\n\n        return categories;\n    }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs",
    "content": "﻿using System.Reflection;\n\nnamespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers;\n\nusing Models;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class HealthController : ControllerBase\n{\n    private static readonly DateTime startedAt = DateTime.Now;\n    private static readonly Assembly assembly = Assembly.GetExecutingAssembly();\n\n    // GET /health\n    [HttpGet]\n    [Route(\"/health\", Name = nameof(Health))]\n    public MicroserviceResult Health()\n    {\n        // Analyze integrated services, get their health and return the Health flag\n        bool isHealthy = true;\n\n        // Get the link of the first action of current microservice workflow\n        var link = Url.RouteUrl(routeName: \"GetWeatherForecast\", values: null, protocol: Request.Scheme);\n\n        return new HealthResult\n        {\n            Healthy = isHealthy,\n            Next = new Uri(link),\n        };\n    }\n\n    // GET /ready\n    [HttpGet]\n    [Route(\"/ready\", Name = nameof(Ready))]\n    public MicroserviceResult Ready()\n    {\n        var asmName = assembly.GetName();\n\n        //var link = Url.Action(action: nameof(Health), controller: nameof(Health), values: null, protocol: Request.Scheme);\n        //var link = Url.RouteUrl(routeName: nameof(Health), values: null, protocol: Request.Scheme);\n        var link = Url.Link(nameof(Health), null);\n\n        return new ReadyResult\n        {\n            ServiceName = asmName.Name,\n            ServiceVersion = asmName.Version.ToString(),\n            StartedAt = startedAt,\n            Next = new Uri(link),\n        };\n    }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs",
    "content": "﻿namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers;\n\nusing Models;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class WeatherForecastController : ControllerBase\n{\n    private static readonly string[] Summaries = new[]\n    {\n        \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\"\n    };\n\n    private readonly ILogger<WeatherForecastController> _logger;\n\n    public WeatherForecastController(ILogger<WeatherForecastController> logger)\n    {\n        _logger = logger;\n    }\n\n    [HttpGet(Name = \"GetWeatherForecast\")]\n    public IEnumerable<WeatherForecast> Get()\n    {\n        return Enumerable.Range(1, 5).Select(index => new WeatherForecast\n        {\n            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),\n            TemperatureC = Random.Shared.Next(-20, 55),\n            Summary = Summaries[Random.Shared.Next(Summaries.Length)]\n        })\n        .ToArray();\n    }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs",
    "content": "﻿namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models;\n\npublic class HealthResult : MicroserviceResult\n{\n    public bool Healthy { get; set; }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs",
    "content": "﻿namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models;\n\npublic class MicroserviceResult\n{\n    public Uri Next { get; set; }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs",
    "content": "﻿using System;\n\nnamespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models;\n\npublic class ReadyResult : MicroserviceResult\n{\n    public string ServiceName { get; set; }\n    public string ServiceVersion { get; set; }\n    public DateTime StartedAt { get; set; }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs",
    "content": "﻿namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models;\n\npublic class WeatherForecast\n{\n    public DateOnly Date { get; set; }\n\n    public int TemperatureC { get; set; }\n\n    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);\n\n    public string Summary { get; set; }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Leon Lucardie, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Swashbuckle.AspNetCore\" Version=\"10.1.5\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Program.cs",
    "content": "﻿global using Microsoft.AspNetCore.Mvc;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\n[assembly: ApiController]\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services\n    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle\n    .AddEndpointsApiExplorer()\n    .AddSwaggerGen()\n\n    .AddHttpClient() // to keep performance of HTTP Client high\n    .AddControllers()\n    .AddJsonOptions(options =>\n    {\n        options.AllowInputFormatterExceptionMessages = true;\n        var jOptions = options.JsonSerializerOptions;\n        jOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));\n        jOptions.PropertyNameCaseInsensitive = true;\n        jOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;\n    });\n\nif (builder.Environment.IsDevelopment())\n{\n    builder.Logging.AddConsole();\n}\n\nvar app = builder.Build();\nif (app.Environment.IsDevelopment())\n{\n    app.UseSwagger();\n    app.UseSwaggerUI();\n    app.UseDeveloperExceptionPage();\n}\nelse\n{\n    app.UseHttpsRedirection();\n}\n\napp.UseRouting();\napp.MapControllers();\napp.Run();\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5564\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7786;http://localhost:5564\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7786/swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7786;http://localhost:5564\"\n      },\n      \"distributionName\": \"\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}/swagger\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62564/\",\n      \"sslPort\": 44364\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/ServiceDiscovery/DownstreamService/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"10.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"8.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"jDM3a95WerM8g6IcMiBXq1qRS9dqmEUpgnCk2DeMWpPkYtp1ia+CkXabOnK93JmhVlUmv8l9WMPsCSUm+WqkIA==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Swashbuckle.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.1.5, )\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.ApiDescription.Server\": \"9.0.0\",\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerGen\": \"10.1.5\",\n          \"Swashbuckle.AspNetCore.SwaggerUI\": \"10.1.5\"\n        }\n      },\n      \"Microsoft.Extensions.ApiDescription.Server\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.0\",\n        \"contentHash\": \"1Kzzf7pRey40VaUkHN9/uWxrKVkLu2AQjt+GVeeKLLpiEHAJ1xZRsLSh4ZZYEnyS7Kt2OBOPmsXNdU+wbcOl5w==\"\n      },\n      \"Microsoft.OpenApi\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.1\",\n        \"contentHash\": \"u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==\"\n      },\n      \"Swashbuckle.AspNetCore.Swagger\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==\",\n        \"dependencies\": {\n          \"Microsoft.OpenApi\": \"2.4.1\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerGen\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==\",\n        \"dependencies\": {\n          \"Swashbuckle.AspNetCore.Swagger\": \"10.1.5\"\n        }\n      },\n      \"Swashbuckle.AspNetCore.SwaggerUI\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.1.5\",\n        \"contentHash\": \"tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.6.33723.286\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceDiscovery.ApiGateway\", \"ApiGateway\\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj\", \"{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ocelot.Samples.ServiceDiscovery.DownstreamService\", \"DownstreamService\\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj\", \"{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {2C604707-2EA1-4CCF-A89C-22B613052C8D}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "samples/ServiceDiscovery/README.md",
    "content": "# Ocelot Service Discovery Custom Provider\n> An example how to build custom service discovery in Ocelot.<br/>\n> **Documentation**: [Service Discovery](../../docs/features/servicediscovery.rst) > [Custom Providers](../../docs/features/servicediscovery.rst#custom-providers)\n\nThis sample constains a basic setup using a custom service discovery provider.<br/>\n\n## Instructions\n    \n### 1. Run Downstream Service app\n```shell\ncd ./DownstreamService/\ndotnet run\n```\nLeave the service running.\n\n### 2. Run API Gateway app\n```shell\ncd ./ApiGateway/\ndotnet run\n```\nLeave the gateway running.\n\n### 3. Make a HTTP request\nTo the URL: http://localhost:5000/categories<br/>\nYou should get the following response:\n```json\n{\n  [ \"category1\", \"category2\" ]\n}\n```\n"
  },
  {
    "path": "samples/ServiceFabric/.gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Service fabric\nOcelotApplicationApiGatewayPkg/Code\nOcelotApplication/OcelotApplicationApiGatewayPkg/Code/appsettings.json\nOcelotApplication/OcelotApplicationApiGatewayPkg/Code/ocelot.json\nOcelotApplication/OcelotApplicationApiGatewayPkg/Code/runtimes/\nOcelotApplicationServicePkg/Code\nOcelotApplication/OcelotApplicationApiGatewayPkg/Code/web.config\nOcelotApplication/OcelotApplicationServicePkg/Code/runtimes/\n!entryPoint.cmd\n!entryPoint.sh\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# Dotnet generated files\n*.dll\n*.pdb\n*.deps.json\n*.runtimeconfig.json\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <Description>Stateless Web Service for Stateful OcelotApplicationApiGateway App</Description>\n    <Platforms>Any CPU;x64</Platforms>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.ServiceFabric\" Version=\"11.3.475\" />\n    <PackageReference Include=\"Microsoft.ServiceFabric.Services\" Version=\"8.3.475\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs",
    "content": "// ------------------------------------------------------------\n//  Copyright (c) Microsoft Corporation.  All rights reserved.\n//  Licensed under the MIT License (MIT). See License.txt in the repo root for license information.\n// ------------------------------------------------------------\n\nusing Microsoft.ServiceFabric.Services.Communication.Runtime;\nusing Microsoft.ServiceFabric.Services.Runtime;\nusing System.Fabric;\n\nnamespace Ocelot.Samples.ServiceFabric.ApiGateway;\n\n/// Service that handles front-end web requests and acts as a proxy to the back-end data for the UI web page.\n/// It is a stateless service that hosts a Web API application on OWIN.\ninternal sealed class OcelotServiceWebService : StatelessService\n{\n    public OcelotServiceWebService(StatelessServiceContext context)\n        : base(context)\n    { }\n\n    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()\n    {\n        return new[]\n        {\n            new ServiceInstanceListener(\n                initparams => new WebCommunicationListener(string.Empty, initparams),\n                \"OcelotServiceWebListener\")\n        };\n    }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/Program.cs",
    "content": "using Microsoft.ServiceFabric.Services.Runtime;\nusing Ocelot.Samples.ServiceFabric.ApiGateway;\nusing System.Diagnostics.Tracing;\n\n// The service host is the executable that hosts the Service instances.\n// Create Service Fabric runtime and register the service type.\ntry\n{\n    //Creating a new event listener to redirect the traces to a file\n    var listener = new ServiceEventListener(\"OcelotApplicationApiGateway\");\n    listener.EnableEvents(ServiceEventSource.Current, EventLevel.LogAlways, EventKeywords.All);\n\n    // The ServiceManifest.XML file defines one or more service type names.\n    // Registering a service maps a service type name to a .NET type.\n    // When Service Fabric creates an instance of this service type,\n    // an instance of the class is created in this host process.\n    await ServiceRuntime.RegisterServiceAsync(\"OcelotApplicationApiGatewayType\", context => new OcelotServiceWebService(context));\n\n    // Prevents this host process from terminating so services keep running.\n    Thread.Sleep(Timeout.Infinite);\n}\ncatch (Exception ex)\n{\n    ServiceEventSource.Current.ServiceHostInitializationFailed(ex);\n    throw;\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:5565\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7787;http://localhost:5565\"\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"https://localhost:7787/swagger\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"ASPNETCORE_URLS\": \"https://localhost:7787;http://localhost:5565\"\n      },\n      \"distributionName\": \"\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}/swagger\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  },\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62565/\",\n      \"sslPort\": 44365\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/ServiceEventListener.cs",
    "content": "// ------------------------------------------------------------\n//  Copyright (c) Microsoft Corporation.  All rights reserved.\n//  Licensed under the MIT License (MIT). See License.txt in the repo root for license information.\n// ------------------------------------------------------------\n\nusing System.Diagnostics.Tracing;\nusing System.Globalization;\nusing System.Text;\n\nnamespace Ocelot.Samples.ServiceFabric.ApiGateway;\n\n/// <summary>\n/// ServiceEventListener is a class which listens to the eventsources registered and redirects the traces to a file\n/// Note that this class serves as a template to EventListener class and redirects the logs to /tmp/{appnameyyyyMMddHHmmssffff}.\n/// You can extend the functionality by writing your code to implement rolling logs for the logs written through this class.\n/// You can also write your custom listener class and handle the registered evestsources accordingly. \n/// </summary>\ninternal class ServiceEventListener : EventListener\n{\n    private readonly string _fileName;\n    private readonly string _filepath = Path.GetTempPath();\n\n    public ServiceEventListener(string appName)\n    {\n        _fileName = appName + DateTime.Now.ToString(\"yyyyMMddHHmmssffff\");\n    }\n\n    /// <summary>\n    /// We override this method to get a callback on every event we subscribed to with EnableEvents\n    /// </summary>\n    /// <param name=\"eventData\">The event arguments that describe the event.</param>\n    protected override void OnEventWritten(EventWrittenEventArgs eventData)\n    {\n        using var writer = new StreamWriter(new FileStream(_filepath + _fileName, FileMode.Append));\n        // report all event information\n        writer.Write(\" {0} \", Write(\n            eventData.Task.ToString(),\n            eventData.EventName!,\n            eventData.EventId.ToString(),\n            eventData.Level));\n        if (eventData.Message != null)\n        {\n            writer.WriteLine(string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload!.ToArray()));\n        }\n    }\n\n    private static String Write(string taskName, string eventName, string id, EventLevel level)\n    {\n        var output = new StringBuilder();\n\n        var now = DateTime.UtcNow;\n        output.Append(now.ToString(\"yyyy/MM/dd-HH:mm:ss.fff\", CultureInfo.InvariantCulture));\n        output.Append(',');\n        output.Append(ConvertLevelToString(level));\n        output.Append(',');\n        output.Append(taskName);\n\n        if (!string.IsNullOrEmpty(eventName))\n        {\n            output.Append('.');\n            output.Append(eventName);\n        }\n\n        if (!string.IsNullOrEmpty(id))\n        {\n            output.Append('@');\n            output.Append(id);\n        }\n\n        output.Append(',');\n        return output.ToString();\n    }\n\n    private static string ConvertLevelToString(EventLevel level)\n    {\n        return level switch\n        {\n            EventLevel.Informational => \"Info\",\n            _ => level.ToString(),\n        };\n    }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/ServiceEventSource.cs",
    "content": "// ------------------------------------------------------------\n//  Copyright (c) Microsoft Corporation.  All rights reserved.\n//  Licensed under the MIT License (MIT). See License.txt in the repo root for license information.\n// ------------------------------------------------------------\n\nusing System.Diagnostics.Tracing;\n\nnamespace Ocelot.Samples.ServiceFabric.ApiGateway;\n\n/// <summary>\n/// Implements methods for logging service related events.\n/// </summary>\npublic class ServiceEventSource : EventSource\n{\n    public static ServiceEventSource Current = new();\n\n    // Define an instance method for each event you want to record and apply an [Event] attribute to it.\n    // The method name is the name of the event.\n    // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed).\n    // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event.\n    // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent().\n    // Put [NonEvent] attribute on all methods that do not define an event.\n    // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx\n\n    [NonEvent]\n    public void Message(string message, params object[] args)\n    {\n        if (IsEnabled())\n        {\n            var finalMessage = string.Format(message, args);\n            Message(finalMessage);\n        }\n    }\n\n    private const int MessageEventId = 1;\n\n    [Event(MessageEventId, Level = EventLevel.Informational, Message = \"{0}\")]\n    public void Message(string message)\n    {\n        if (IsEnabled())\n        {\n            WriteEvent(MessageEventId, message);\n        }\n    }\n\n    private const int ServiceTypeRegisteredEventId = 3;\n\n    [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = \"Service host process {0} registered service type {1}\")]\n    public void ServiceTypeRegistered(int hostProcessId, string serviceType)\n    {\n        WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType);\n    }\n\n    [NonEvent]\n    public void ServiceHostInitializationFailed(Exception e)\n    {\n        ServiceHostInitializationFailed(e.ToString());\n    }\n\n    private const int ServiceHostInitializationFailedEventId = 4;\n\n    [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = \"Service host initialization failed: {0}\")]\n    private void ServiceHostInitializationFailed(string exception)\n    {\n        WriteEvent(ServiceHostInitializationFailedEventId, exception);\n    }\n\n    [NonEvent]\n    public void ServiceWebHostBuilderFailed(Exception e)\n    {\n        ServiceWebHostBuilderFailed(e.ToString());\n    }\n\n    private const int ServiceWebHostBuilderFailedEventId = 5;\n\n    [Event(ServiceWebHostBuilderFailedEventId, Level = EventLevel.Error, Message = \"Service Owin Web Host Builder Failed: {0}\")]\n    private void ServiceWebHostBuilderFailed(string exception)\n    {\n        WriteEvent(ServiceWebHostBuilderFailedEventId, exception);\n    }\n\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs",
    "content": "﻿using Microsoft.ServiceFabric.Services.Communication.Runtime;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Samples.Web;\nusing System.Fabric;\nusing System.Globalization;\n\nnamespace Ocelot.Samples.ServiceFabric.ApiGateway;\n\npublic class WebCommunicationListener : ICommunicationListener\n{\n    private readonly string _appRoot;\n    private readonly ServiceContext _serviceInitializationParameters;\n    private string _listeningAddress;\n    private string _publishAddress;\n\n    // OWIN server handle.\n    private WebApplication _webApp;\n\n    public WebCommunicationListener(string appRoot, ServiceContext serviceInitializationParameters)\n    {\n        _appRoot = appRoot;\n        _serviceInitializationParameters = serviceInitializationParameters;\n    }\n\n    public async Task<string> OpenAsync(CancellationToken cancellationToken)\n    {\n        ServiceEventSource.Current.Message(\"Initialize\");\n\n        var serviceEndpoint = _serviceInitializationParameters.CodePackageActivationContext.GetEndpoint(\"WebEndpoint\");\n        var port = serviceEndpoint.Port;\n\n        _listeningAddress = string.Format(\n            CultureInfo.InvariantCulture,\n            \"http://+:{0}/{1}\",\n            port,\n            string.IsNullOrWhiteSpace(_appRoot)\n                ? string.Empty\n                : _appRoot.TrimEnd('/') + '/');\n\n        _publishAddress = _listeningAddress.Replace(\"+\", FabricRuntime.GetNodeContext().IPAddressOrFQDN);\n\n        ServiceEventSource.Current.Message(\"Starting web server on {0}\", _listeningAddress);\n\n        try\n        {\n            _ = OcelotHostBuilder.Create();\n            var builder = WebApplication.CreateBuilder(); //(args);\n            builder.WebHost.UseUrls(_listeningAddress);\n            builder.Configuration\n                .SetBasePath(builder.Environment.ContentRootPath)\n                .AddOcelot();\n            builder.Services\n                .AddOcelot(builder.Configuration);\n            if (builder.Environment.IsDevelopment())\n            {\n                builder.Logging.AddConsole();\n            }\n\n            _webApp = builder.Build();\n            await _webApp.UseOcelot();\n            await _webApp.RunAsync(); // .Start();\n        }\n        catch (Exception ex)\n        {\n            ServiceEventSource.Current.ServiceWebHostBuilderFailed(ex);\n        }\n        return _publishAddress;\n    }\n\n    public Task CloseAsync(CancellationToken cancellationToken) => StopAll(cancellationToken);\n    public void Abort() => StopAll().GetAwaiter().GetResult();\n\n    /// <summary>Stops, cancels, and disposes everything.</summary>\n    private Task StopAll(CancellationToken cancellationToken = default)\n    {\n        try\n        {\n            if (_webApp != null)\n            {\n                ServiceEventSource.Current.Message(\"Stopping web server.\");\n                return _webApp.StopAsync(cancellationToken);\n            }\n        }\n        catch (ObjectDisposedException)\n        {\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/ocelot.json",
    "content": "﻿{\n  \"Routes\": [\n    {\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamPathTemplate\": \"/api/values\",\n      \"UpstreamPathTemplate\": \"/EquipmentInterfaces\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"ServiceName\": \"OcelotServiceApplication/OcelotApplicationService\"\n    }\n  ],\n  \"GlobalConfiguration\": {\n    \"RequestIdKey\": \"Oc-RequestId\",\n    \"ServiceDiscoveryProvider\": {\n      \"Host\": \"localhost\",\n      \"Port\": 19081,\n      \"Type\": \"ServiceFabric\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/ApiGateway/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\"\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\"\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\"\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/ServiceFabric/CONTRIBUTING.md",
    "content": "# Contributing to Azure samples\n\nThank you for your interest in contributing to Azure samples!\n\n## Ways to contribute\n\nYou can contribute to [Azure samples](https://azure.microsoft.com/documentation/samples/) in a few different ways:\n\n- Submit feedback on [this sample page](https://azure.microsoft.com/documentation/samples/service-fabric-dotnet-web-reference-app/) whether it was helpful or not.  \n- Submit issues through [issue tracker](https://github.com/Azure-Samples/service-fabric-dotnet-web-reference-app/issues) on GitHub. We are actively monitoring the issues and improving our samples.\n- If you wish to make code changes to samples, or contribute something new, please follow the [GitHub Forks / Pull requests model](https://help.github.com/articles/fork-a-repo/): Fork the sample repo, make the change and propose it back by submitting a pull request.\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/ApiGateway.cs",
    "content": "using Microsoft.ServiceFabric.Services.Communication.AspNetCore;\nusing Microsoft.ServiceFabric.Services.Communication.Runtime;\nusing Microsoft.ServiceFabric.Services.Runtime;\nusing Ocelot.Samples.Web;\nusing System.Fabric;\n\nnamespace Ocelot.Samples.ServiceFabric.DownstreamService;\n\n/// <summary>\n/// The FabricRuntime creates an instance of this class for each service type instance. \n/// </summary>\ninternal sealed class ApiGateway : StatelessService\n{\n    public ApiGateway(StatelessServiceContext context)\n        : base(context) { }\n\n    /// <summary>\n    /// Optional override to create listeners (like tcp, http) for this service instance.\n    /// </summary>\n    /// <returns>The collection of listeners.</returns>\n    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()\n    {\n        return new ServiceInstanceListener[]\n        {\n            new(serviceContext =>\n                new KestrelCommunicationListener(serviceContext, \"ServiceEndpoint\", (url, listener) =>\n                {\n                    Console.WriteLine($\"Starting Kestrel on {url}\");\n                    ServiceEventSource.Current.ServiceMessage(serviceContext, $\"Starting Kestrel on {url}\");\n\n                    _ = OcelotHostBuilder.Create();\n                    var builder = WebApplication.CreateBuilder(); //(args);\n                    builder.Services\n                        .AddSingleton(serviceContext)\n                        .AddControllers();\n                    builder.WebHost\n                    //.UseKestrel()\n                        .UseContentRoot(Directory.GetCurrentDirectory())\n                        .UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.UseUniqueServiceUrl)\n                        .UseUrls(url);\n                    if (builder.Environment.IsDevelopment())\n                    {\n                        builder.Logging.AddConsole();\n                    }\n                    var app = builder.Build();\n                    app.MapControllers();\n                    if (app.Environment.IsDevelopment())\n                    {\n                        app.UseDeveloperExceptionPage();\n                    }\n                    return app;\n                }))\n        };\n    }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs",
    "content": "using Microsoft.AspNetCore.Mvc;\n\nnamespace Ocelot.Samples.ServiceFabric.DownstreamService.Controllers;\n\n[Route(\"api/[controller]\")]\npublic class ValuesController : Controller\n{\n    // GET api/values\n    [HttpGet]\n    public IEnumerable<string> Get()\n    {\n        return new[] { \"value1\", \"value2\" };\n    }\n\n    // GET api/values/5\n    [HttpGet(\"{id}\")]\n    public string Get(int id)\n    {\n        return \"value\";\n    }\n\n    // POST api/values\n    [HttpPost]\n    public void Post([FromBody] string value)\n    {\n    }\n\n    // PUT api/values/5\n    [HttpPut(\"{id}\")]\n    public void Put(int id, [FromBody] string value)\n    {\n    }\n\n    // DELETE api/values/5\n    [HttpDelete(\"{id}\")]\n    public void Delete(int id)\n    {\n    }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <Description>Stateless Service Application</Description>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <PlatformTarget>x64</PlatformTarget>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric</PackageProjectUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.ServiceFabric\" Version=\"11.3.475\" />\n    <PackageReference Include=\"Microsoft.ServiceFabric.Services\" Version=\"8.3.475\" />\n    <PackageReference Include=\"Microsoft.ServiceFabric.AspNetCore.Kestrel\" Version=\"8.3.475\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\Web\\Ocelot.Samples.Web.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/Program.cs",
    "content": "using Microsoft.ServiceFabric.Services.Runtime;\nusing Ocelot.Samples.ServiceFabric.DownstreamService;\n\ntry\n{\n    // The ServiceManifest.XML file defines one or more service type names.\n    // Registering a service maps a service type name to a .NET type.\n    // When Service Fabric creates an instance of this service type,\n    // an instance of the class is created in this host process.\n    await ServiceRuntime.RegisterServiceAsync(\"OcelotApplicationServiceType\",\n        (context) => new ApiGateway(context));\n\n    ServiceEventSource.Current.ServiceTypeRegistered(Environment.ProcessId, nameof(ApiGateway));\n\n    // Prevents this host process from terminating so services keeps running. \n    Thread.Sleep(Timeout.Infinite);\n}\ncatch (Exception e)\n{\n    ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString());\n    throw;\n}\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"api/values\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:7788;http://localhost:5566\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/ServiceEventSource.cs",
    "content": "using System.Diagnostics.Tracing;\nusing System.Fabric;\n\nnamespace Ocelot.Samples.ServiceFabric.DownstreamService;\n\n[EventSource(Name = \"MyCompany-ServiceOcelotApplication-OcelotService\")]\ninternal sealed class ServiceEventSource : EventSource\n{\n    public static readonly ServiceEventSource Current = new();\n\n    static ServiceEventSource()\n    {\n        // A workaround for the problem where ETW activities do not get tracked until Tasks infrastructure is initialized.\n        // This problem will be fixed in .NET Framework 4.6.2.\n        Task.Run(() => { });\n    }\n\n    // Instance constructor is private to enforce singleton semantics\n    private ServiceEventSource() : base() { }\n\n    #region Keywords\n    // Event keywords can be used to categorize events. \n    // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property).\n    // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them.\n    public static class Keywords\n    {\n        public const EventKeywords Requests = (EventKeywords)0x1L;\n        public const EventKeywords ServiceInitialization = (EventKeywords)0x2L;\n    }\n    #endregion\n\n    #region Events\n    // Define an instance method for each event you want to record and apply an [Event] attribute to it.\n    // The method name is the name of the event.\n    // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed).\n    // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event.\n    // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent().\n    // Put [NonEvent] attribute on all methods that do not define an event.\n    // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx\n\n    [NonEvent]\n    public void Message(string message, params object[] args)\n    {\n        if (IsEnabled())\n        {\n            var finalMessage = string.Format(message, args);\n            Message(finalMessage);\n        }\n    }\n\n    private const int MessageEventId = 1;\n    [Event(MessageEventId, Level = EventLevel.Informational, Message = \"{0}\")]\n    public void Message(string message)\n    {\n        if (IsEnabled())\n        {\n            WriteEvent(MessageEventId, message);\n        }\n    }\n\n    [NonEvent]\n    public void ServiceMessage(ServiceContext serviceContext, string message, params object[] args)\n    {\n        if (IsEnabled())\n        {\n\n            var finalMessage = string.Format(message, args);\n            ServiceMessage(\n                serviceContext.ServiceName.ToString(),\n                serviceContext.ServiceTypeName,\n                GetReplicaOrInstanceId(serviceContext),\n                serviceContext.PartitionId,\n                serviceContext.CodePackageActivationContext.ApplicationName,\n                serviceContext.CodePackageActivationContext.ApplicationTypeName,\n                serviceContext.NodeContext.NodeName,\n                finalMessage);\n        }\n    }\n\n    // For very high-frequency events it might be advantageous to raise events using WriteEventCore API.\n    // This results in more efficient parameter handling, but requires explicit allocation of EventData structure and unsafe code.\n    // To enable this code path, define UNSAFE conditional compilation symbol and turn on unsafe code support in project properties.\n    private const int ServiceMessageEventId = 2;\n    [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = \"{7}\")]\n    private\n#if UNSAFE\n    unsafe\n#endif\n    void ServiceMessage(\n        string serviceName,\n        string serviceTypeName,\n        long replicaOrInstanceId,\n        Guid partitionId,\n        string applicationName,\n        string applicationTypeName,\n        string nodeName,\n        string message)\n    {\n#if !UNSAFE\n        WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message);\n#else\n        const int numArgs = 8;\n        fixed (char* pServiceName = serviceName, pServiceTypeName = serviceTypeName, pApplicationName = applicationName, pApplicationTypeName = applicationTypeName, pNodeName = nodeName, pMessage = message)\n        {\n            EventData* eventData = stackalloc EventData[numArgs];\n            eventData[0] = new EventData { DataPointer = (IntPtr) pServiceName, Size = SizeInBytes(serviceName) };\n            eventData[1] = new EventData { DataPointer = (IntPtr) pServiceTypeName, Size = SizeInBytes(serviceTypeName) };\n            eventData[2] = new EventData { DataPointer = (IntPtr) (&replicaOrInstanceId), Size = sizeof(long) };\n            eventData[3] = new EventData { DataPointer = (IntPtr) (&partitionId), Size = sizeof(Guid) };\n            eventData[4] = new EventData { DataPointer = (IntPtr) pApplicationName, Size = SizeInBytes(applicationName) };\n            eventData[5] = new EventData { DataPointer = (IntPtr) pApplicationTypeName, Size = SizeInBytes(applicationTypeName) };\n            eventData[6] = new EventData { DataPointer = (IntPtr) pNodeName, Size = SizeInBytes(nodeName) };\n            eventData[7] = new EventData { DataPointer = (IntPtr) pMessage, Size = SizeInBytes(message) };\n\n            WriteEventCore(ServiceMessageEventId, numArgs, eventData);\n        }\n#endif\n    }\n\n    private const int ServiceTypeRegisteredEventId = 3;\n    [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = \"Service host process {0} registered service type {1}\", Keywords = Keywords.ServiceInitialization)]\n    public void ServiceTypeRegistered(int hostProcessId, string serviceType)\n    {\n        WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType);\n    }\n\n    private const int ServiceHostInitializationFailedEventId = 4;\n    [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = \"Service host initialization failed\", Keywords = Keywords.ServiceInitialization)]\n    public void ServiceHostInitializationFailed(string exception)\n    {\n        WriteEvent(ServiceHostInitializationFailedEventId, exception);\n    }\n\n    // A pair of events sharing the same name prefix with a \"Start\"/\"Stop\" suffix implicitly marks boundaries of an event tracing activity.\n    // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities,\n    // and other statistics.\n    private const int ServiceRequestStartEventId = 5;\n    [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = \"Service request '{0}' started\", Keywords = Keywords.Requests)]\n    public void ServiceRequestStart(string requestTypeName)\n    {\n        WriteEvent(ServiceRequestStartEventId, requestTypeName);\n    }\n\n    private const int ServiceRequestStopEventId = 6;\n    [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = \"Service request '{0}' finished\", Keywords = Keywords.Requests)]\n    public void ServiceRequestStop(string requestTypeName, string exception = \"\")\n    {\n        WriteEvent(ServiceRequestStopEventId, requestTypeName, exception);\n    }\n    #endregion\n\n    #region Private methods\n    private static long GetReplicaOrInstanceId(ServiceContext context)\n    {\n        if (context is StatelessServiceContext stateless)\n        {\n            return stateless.InstanceId;\n        }\n\n        if (context is StatefulServiceContext stateful)\n        {\n            return stateful.ReplicaId;\n        }\n\n        throw new NotSupportedException(\"Context type not supported.\");\n    }\n#if UNSAFE\n    private int SizeInBytes(string s)\n    {\n        if (s == null)\n        {\n            return 0;\n        }\n        else\n        {\n            return (s.Length + 1) * sizeof(char);\n        }\n    }\n#endif\n    #endregion\n}\n"
  },
  {
    "path": "samples/ServiceFabric/DownstreamService/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\",\n        \"dependencies\": {\n          \"System.Memory\": \"4.5.5\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Kestrel\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Services\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"System.Memory\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.5.5\",\n        \"contentHash\": \"XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net8.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\",\n        \"dependencies\": {\n          \"System.Memory\": \"4.5.5\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Kestrel\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Services\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"System.Memory\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.5.5\",\n        \"contentHash\": \"XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    },\n    \"net9.0\": {\n      \"Microsoft.ServiceFabric\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[11.3.475, )\",\n        \"resolved\": \"11.3.475\",\n        \"contentHash\": \"PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==\",\n        \"dependencies\": {\n          \"System.Memory\": \"4.5.5\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Kestrel\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Services\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.3.475, )\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Data\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Diagnostics.Internal\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.AspNetCore.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric.Services\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Extensions\": \"[8.3.475]\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Extensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"11.3.475\",\n          \"Microsoft.ServiceFabric.Data.Interfaces\": \"[8.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Data.Interfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"Microsoft.ServiceFabric.Diagnostics.Internal\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.3.475\",\n        \"contentHash\": \"yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==\",\n        \"dependencies\": {\n          \"Microsoft.ServiceFabric\": \"[11.3.475]\"\n        }\n      },\n      \"System.Memory\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.5.5\",\n        \"contentHash\": \"XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==\"\n      },\n      \"ocelot.samples.web\": {\n        \"type\": \"Project\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "samples/ServiceFabric/LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2016 Azure Samples\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "samples/ServiceFabric/Ocelot.Samples.ServiceFabric.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.13.35919.96\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.ServiceFabric.ApiGateway\", \"ApiGateway\\Ocelot.Samples.ServiceFabric.ApiGateway.csproj\", \"{D951C29E-FBB5-175A-83B8-41477A3B45C6}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.ServiceFabric.DownstreamService\", \"DownstreamService\\Ocelot.Samples.ServiceFabric.DownstreamService.csproj\", \"{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot.Samples.Web\", \"..\\Web\\Ocelot.Samples.Web.csproj\", \"{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ocelot\", \"..\\..\\src\\Ocelot\\Ocelot.csproj\", \"{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{D951C29E-FBB5-175A-83B8-41477A3B45C6}.Debug|Any CPU.ActiveCfg = Debug|x64\n\t\t{D951C29E-FBB5-175A-83B8-41477A3B45C6}.Debug|Any CPU.Build.0 = Debug|x64\n\t\t{D951C29E-FBB5-175A-83B8-41477A3B45C6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D951C29E-FBB5-175A-83B8-41477A3B45C6}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {17D35415-616B-4BAA-A82F-37F7BAC0FC77}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApplicationManifest ApplicationTypeName=\"OcelotServiceApplicationType\"\n                     ApplicationTypeVersion=\"1.0.0\"\n                     xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                     xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                     xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <Parameters>\n    <Parameter Name=\"OcelotApplicationService_InstanceCount\" DefaultValue=\"1\" />\n    <Parameter Name=\"OcelotApplicationApiGateway_InstanceCount\" DefaultValue=\"1\" />\n  </Parameters>\n  <!-- Import the ServiceManifest from the ServicePackage. The ServiceManifestName and ServiceManifestVersion\n       should match the Name and Version attributes of the ServiceManifest element defined in the\n       ServiceManifest.xml file. -->\n  <ServiceManifestImport>\n    <ServiceManifestRef ServiceManifestName=\"OcelotApplicationServicePkg\" ServiceManifestVersion=\"1.0.0\" />\n    <ConfigOverrides />\n  </ServiceManifestImport>\n  <ServiceManifestImport>\n    <ServiceManifestRef ServiceManifestName=\"OcelotApplicationApiGatewayPkg\" ServiceManifestVersion=\"1.0.0\" />\n    <ConfigOverrides />\n  </ServiceManifestImport>\n  <DefaultServices>\n    <!-- The section below creates instances of service types, when an instance of this\n         application type is created. You can also create one or more instances of service type using the\n         ServiceFabric PowerShell module.\n\n         The attribute ServiceTypeName below must match the name defined in the imported ServiceManifest.xml file. -->\n    <Service Name=\"OcelotApplicationService\">\n      <StatelessService ServiceTypeName=\"OcelotApplicationServiceType\" InstanceCount=\"[OcelotApplicationService_InstanceCount]\">\n        <SingletonPartition />\n      </StatelessService>\n    </Service>\n    <Service Name=\"OcelotApplicationApiGateway\">\n      <StatelessService ServiceTypeName=\"OcelotApplicationApiGatewayType\" InstanceCount=\"[OcelotApplicationApiGateway_InstanceCount]\">\n        <SingletonPartition />\n      </StatelessService>\n    </Service>\n  </DefaultServices>\n</ApplicationManifest>\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd",
    "content": "dotnet %~dp0\\OcelotApplicationApiGateway.dll\nexit /b %errorlevel%"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh",
    "content": "#!/usr/bin/env bash\ncheck_errs()\n{\n  # Function. Parameter 1 is the return code\n  if [ \"${1}\" -ne \"0\" ]; then\n    # make our script exit with the right error code.\n    exit ${1}\n  fi\n}\n\nDIR=`dirname $0`\n\necho 0x3f > /proc/self/coredump_filter\n\nsource $DIR/dotnet-include.sh \ndotnet $DIR/OcelotApplicationApiGateway.dll $@\ncheck_errs $?\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<Settings xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.microsoft.com/2011/01/fabric\">\n  <!-- Add your custom configuration sections and parameters here -->\n  <!--\n  <Section Name=\"MyConfigSection\">\n    <Parameter Name=\"MyParameter\" Value=\"Value1\" />\n  </Section>\n  -->\n</Settings>\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt",
    "content": "contains a Settings.xml file, that can specify parameters for the service\n\nConfiguration packages describe user-defined, application-overridable configuration settings (sections of key-value pairs)\nrequired for running service replicas/instances of service types specified in the ser-vice manifest. The configuration settings \nmust be stored in Settings.xml in the config package folder. \n\nThe service developer uses Service Fabric APIs to locate the package folders and read applica-tion-overridable configuration settings.\nThe service developer can also register callbacks that are in-voked when any of the configuration packages specified in the\nservice manifest are upgraded and re-reads new configuration settings inside that callback. \n\nThis means that Service Fabric will not recycle EXEs and DLLHOSTs specified in the host and support packages when\nconfiguration packages are up-graded. "
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt",
    "content": "Data packages contain data files like custom dictionaries, \nnon-overridable configuration files, custom initialized data files, etc. \n\nService Fabric will recycle all EXEs and DLLHOSTs specified in the host and support packages when any of the data packages\nspecified inside service manifest are upgraded."
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationApiGatewayPkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType. \n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationApiGatewayType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.sh</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an \n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <!-- This endpoint is used by the communication listener to obtain the port on which to \n           listen. Please note that if your service is partitioned, this port is shared with \n           replicas of different partitions that are placed in your code. -->\n      <Endpoint Name=\"WebEndpoint\" Protocol=\"http\" Port=\"31002\" />\n    </Endpoints>\n  </Resources>\n</ServiceManifest>"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationApiGatewayPkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType. \n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationApiGatewayType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.cmd</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an \n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <!-- This endpoint is used by the communication listener to obtain the port on which to \n           listen. Please note that if your service is partitioned, this port is shared with \n           replicas of different partitions that are placed in your code. -->\n      <Endpoint Name=\"WebEndpoint\" Protocol=\"http\" Port=\"31002\" />\n    </Endpoints>\n  </Resources>\n</ServiceManifest>"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationApiGatewayPkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType. \n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationApiGatewayType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.cmd</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an \n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <!-- This endpoint is used by the communication listener to obtain the port on which to \n           listen. Please note that if your service is partitioned, this port is shared with \n           replicas of different partitions that are placed in your code. -->\n      <Endpoint Name=\"WebEndpoint\" Protocol=\"http\" Port=\"31002\" />\n    </Endpoints>\n  </Resources>\n</ServiceManifest>"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd",
    "content": "dotnet %~dp0\\OcelotApplicationService.dll\nexit /b %errorlevel%"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh",
    "content": "#!/usr/bin/env bash\ncheck_errs()\n{\n  # Function. Parameter 1 is the return code\n  if [ \"${1}\" -ne \"0\" ]; then\n    # make our script exit with the right error code.\n    exit ${1}\n  fi\n}\n\nDIR=`dirname $0`\nsource $DIR/dotnet-include.sh \n\ndotnet $DIR/OcelotApplicationService.dll $@\ncheck_errs $?\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<Settings xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.microsoft.com/2011/01/fabric\">\n  <!-- Add your custom configuration sections and parameters here -->\n  <!--\n  <Section Name=\"MyConfigSection\">\n    <Parameter Name=\"MyParameter\" Value=\"Value1\" />\n  </Section>\n  -->\n</Settings>\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt",
    "content": "contains a Settings.xml file, that can specify parameters for the service\n\nConfiguration packages describe user-defined, application-overridable configuration settings (sections of key-value pairs)\nrequired for running service replicas/instances of service types specified in the ser-vice manifest. The configuration settings \nmust be stored in Settings.xml in the config package folder. \n\nThe service developer uses Service Fabric APIs to locate the package folders and read applica-tion-overridable configuration settings.\nThe service developer can also register callbacks that are in-voked when any of the configuration packages specified in the\nservice manifest are upgraded and re-reads new configuration settings inside that callback. \n\nThis means that Service Fabric will not recycle EXEs and DLLHOSTs specified in the host and support packages when\nconfiguration packages are up-graded. "
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt",
    "content": "Data packages contain data files like custom dictionaries,\nnon-overridable configuration files, custom initialized data files, etc.\n\nService Fabric will recycle all EXEs and DLLHOSTs specified in the host and support packages when any of the data packages\nspecified inside service manifest are upgraded."
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationServicePkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType.\n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationServiceType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.sh</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an\n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <Endpoint Name=\"ServiceEndpoint\" Protocol=\"http\"/>\n    </Endpoints>\n  </Resources>\n</ServiceManifest>\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationServicePkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType.\n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationServiceType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.cmd</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an\n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <Endpoint Name=\"ServiceEndpoint\" Protocol=\"http\"/>\n    </Endpoints>\n  </Resources>\n</ServiceManifest>\n"
  },
  {
    "path": "samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ServiceManifest Name=\"OcelotApplicationServicePkg\"\n                 Version=\"1.0.0\"\n                 xmlns=\"http://schemas.microsoft.com/2011/01/fabric\"\n                 xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n  <ServiceTypes>\n    <!-- This is the name of your ServiceType.\n         This name must match the string used in RegisterServiceType call in Program.cs. -->\n    <StatelessServiceType ServiceTypeName=\"OcelotApplicationServiceType\" />\n  </ServiceTypes>\n\n  <!-- Code package is your service executable. -->\n  <CodePackage Name=\"Code\" Version=\"1.0.0\">\n    <EntryPoint>\n      <ExeHost>\n        <Program>entryPoint.cmd</Program>\n        <ConsoleRedirection FileRetentionCount=\"5\" FileMaxSizeInKb=\"2048\"/>\n      </ExeHost>\n    </EntryPoint>\n  </CodePackage>\n\n  <!-- Config package is the contents of the Config directoy under PackageRoot that contains an\n       independently-updateable and versioned set of custom configuration settings for your service. -->\n  <ConfigPackage Name=\"Config\" Version=\"1.0.0\" />\n\n  <Resources>\n    <Endpoints>\n      <Endpoint Name=\"ServiceEndpoint\" Protocol=\"http\"/>\n    </Endpoints>\n  </Resources>\n</ServiceManifest>\n"
  },
  {
    "path": "samples/ServiceFabric/README.md",
    "content": "---\nservices: service-fabric\nplatforms: dotnet\nauthor: raunakpandya edited by Tom Pallister for Ocelot\n---\n\n# Ocelot Service Fabric example\n\nThis shows a service fabric cluster with Ocelot exposed over HTTP accessing services in the cluster via the naming service. If you want to try and use Ocelot with\nService Fabric I reccomend using this as a starting point.\n\nIf you want to use statefull / actors you must send the PartitionKind and PartitionKey to Ocelot as query string parameters.\n\nI have not tested this sample on Service Fabric hosted on Linux just a Windows dev cluster. This sample assumes a good understanding of Service Fabric.\n\nThe rest of this document is from the Microsoft asp.net core service fabric getting started guide.\n\n# Getting started with Service Fabric with .NET Core\n\nThis repository contains a set of simple sample projects to help you getting started with Service Fabric on Linux with .NET Core as the framework. As a pre requisite ensure you have the Service Fabric C# SDK installed on ubuntu box. Follow these instruction to [prepare your development environment on Linux][service-fabric-Linux-getting-started]\n\n### Folder Hierarchy\n* src/ - Source of the application divided by different modules by sub-folders.  \n* &lt;application package folder&gt;/ - Service Fabric Application folder heirarchy. After compilation the executables are placed in code subfolders.  \n* build.sh - Script to build source on Linux shell.  \n* build.ps1 - PowerShell script to build source on Windows.  \n* install.sh - Script to install Application from Linux shell.  \n* install.ps1 - PowerShell script to install application from Windows.  Before calling this script run Connect-ServiceFabricCluster localhost:19000 or however you prefer to connect.\n* uninstall.sh - Script to uninstall application from Linux shell.  \n* uninstall.ps1 - PowerShell script to unintall application from Windows.\n* dotnet-include.sh - Script to conditionally handle RHEL dotnet cli through scl(software collections)\n\n# Testing\n\nOnce everything is up and running on your dev cluster visit http://localhost:31002/EquipmentInterfaces and you should see the following returned.\n\n```json\n[\"value1\",\"value2\"]\n```\n\nIf you get any errors please check the service fabric logs and let me know if you need help.\n\n## More information\n\nThe [Service Fabric documentation][service-fabric-docs] includes a rich set of tutorials and conceptual articles, which serve as a good complement to the samples.\n\n<!-- Links -->\n\n[service-fabric-programming-models]: https://azure.microsoft.com/en-us/documentation/articles/service-fabric-choose-framework/\n[service-fabric-docs]: http://aka.ms/servicefabricdocs\n[service-fabric-Linux-getting-started]: https://azure.microsoft.com/en-us/documentation/articles/service-fabric-get-started-linux/\n"
  },
  {
    "path": "samples/ServiceFabric/build.bat",
    "content": "cd ./src/OcelotApplicationService/\ndotnet restore -s https://api.nuget.org/v3/index.json\ndotnet build \ndotnet publish -o ../../OcelotApplication/OcelotApplicationServicePkg/Code\ncd ../../\n\ncd ./src/OcelotApplicationApiGateway/\ndotnet restore -s https://api.nuget.org/v3/index.json\ndotnet build \ndotnet publish -o ../../OcelotApplication/OcelotApplicationApiGatewayPkg/Code\ncd ../../\n\n"
  },
  {
    "path": "samples/ServiceFabric/build.sh",
    "content": "#!/bin/bash\nDIR=`dirname $0`\nsource $DIR/dotnet-include.sh \n\ncd $DIR/src/OcelotApplicationService/\ndotnet restore -s https://api.nuget.org/v3/index.json\ndotnet build \ndotnet publish -o ../../OcelotApplication/OcelotApplicationServicePkg/Code\ncd -\n\ncd $DIR/src/OcelotApplicationApiGateway/\ndotnet restore -s https://api.nuget.org/v3/index.json\ndotnet build \ndotnet publish -o ../../OcelotApplication/OcelotApplicationApiGatewayPkg/Code\ncd -\n"
  },
  {
    "path": "samples/ServiceFabric/dotnet-include.sh",
    "content": "#!/bin/bash\n\n. /etc/os-release\nlinuxDistrib=$ID\nif [ $linuxDistrib = \"rhel\" ]; then\n  source scl_source enable rh-dotnet20\n  exitCode=$?\n  if [ $exitCode != 0 ]; then\n    echo \"Failed: source scl_source enable rh-dotnet20 : ExitCode: $exitCode\"\n    exit $exitCode\n  fi\nfi\n"
  },
  {
    "path": "samples/ServiceFabric/install.ps1",
    "content": "$AppPath = \"$PSScriptRoot\\OcelotApplication\"\n$sdkInstallPath = (Get-ItemProperty 'HKLM:\\Software\\Microsoft\\Service Fabric SDK').FabricSDKInstallPath\n$sfSdkPsModulePath = $sdkInstallPath + \"Tools\\PSModule\\ServiceFabricSDK\"\nImport-Module $sfSdkPsModulePath\\ServiceFabricSDK.psm1\n\n$StatefulServiceManifestlocation = $AppPath + \"\\OcelotApplicationServicePkg\\\"\n$StatefulServiceManifestlocationLinux = $StatefulServiceManifestlocation + \"\\ServiceManifest-Linux.xml\"\n$StatefulServiceManifestlocationWindows = $StatefulServiceManifestlocation + \"\\ServiceManifest-Windows.xml\"\n$StatefulServiceManifestlocationFinal= $StatefulServiceManifestlocation + \"ServiceManifest.xml\"\nCopy-Item -Path $StatefulServiceManifestlocationWindows -Destination $StatefulServiceManifestlocationFinal -Force\n\n$WebServiceManifestlocation = $AppPath + \"\\OcelotApplicationApiGatewayPkg\\\"\n$WebServiceManifestlocationLinux = $WebServiceManifestlocation + \"\\ServiceManifest-Linux.xml\"\n$WebServiceManifestlocationWindows = $WebServiceManifestlocation + \"\\ServiceManifest-Windows.xml\"\n$WebServiceManifestlocationFinal= $WebServiceManifestlocation + \"ServiceManifest.xml\"\nCopy-Item -Path $WebServiceManifestlocationWindows -Destination $WebServiceManifestlocationFinal -Force\n\nCopy-ServiceFabricApplicationPackage -ApplicationPackagePath $AppPath -ApplicationPackagePathInImageStore OcelotServiceApplicationType -ImageStoreConnectionString (Get-ImageStoreConnectionStringFromClusterManifest(Get-ServiceFabricClusterManifest)) -TimeoutSec 1800\nRegister-ServiceFabricApplicationType OcelotServiceApplicationType\nNew-ServiceFabricApplication fabric:/OcelotServiceApplication OcelotServiceApplicationType 1.0.0"
  },
  {
    "path": "samples/ServiceFabric/install.sh",
    "content": "#!/bin/bash\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nappPkg=\"$DIR/OcelotServiceApplication\"\n\nWebServiceManifestlocation=\"$appPkg/OcelotApplicationApiGatewayPkg\"\nWebServiceManifestlocationLinux=\"$WebServiceManifestlocation/ServiceManifest-Linux.xml\"\nWebServiceManifestlocationWindows=\"$WebServiceManifestlocation/ServiceManifest-Windows.xml\"\nWebServiceManifestlocation=\"$WebServiceManifestlocation/ServiceManifest.xml\"\ncp $WebServiceManifestlocationLinux $WebServiceManifestlocation \n\n\nStatefulServiceManifestlocation=\"$appPkg/OcelotApplicationServicePkg\"\nStatefulServiceManifestlocationLinux=\"$StatefulServiceManifestlocation/ServiceManifest-Linux.xml\"\nStatefulServiceManifestlocationWindows=\"$StatefulServiceManifestlocation/ServiceManifest-Windows.xml\"\nStatefulServiceManifestlocation=\"$StatefulServiceManifestlocation/ServiceManifest.xml\"\ncp $StatefulServiceManifestlocationLinux $StatefulServiceManifestlocation\ncp dotnet-include.sh ./OcelotServiceApplication/OcelotApplicationServicePkg/Code\ncp dotnet-include.sh ./OcelotServiceApplication/OcelotApplicationApiGatewayPkg/Code\nsfctl application upload --path OcelotServiceApplication --show-progress\nsfctl application provision --application-type-build-path OcelotServiceApplication\nsfctl application create --app-name fabric:/OcelotServiceApplication --app-type OcelotServiceApplicationType --app-version 1.0.0\n"
  },
  {
    "path": "samples/ServiceFabric/uninstall.ps1",
    "content": "Remove-ServiceFabricApplication fabric:/OcelotServiceApplication \nUnregister-ServiceFabricApplicationType OcelotServiceApplicationType 1.0.0"
  },
  {
    "path": "samples/ServiceFabric/uninstall.sh",
    "content": "#!/bin/bash -x\n\nsfctl application delete --application-id OcelotServiceApplication\nsfctl application unprovision --application-type-name OcelotServiceApplicationType --application-type-version 1.0.0\nsfctl store delete --content-path OcelotServiceApplication"
  },
  {
    "path": "samples/Web/DownstreamHostBuilder.cs",
    "content": "﻿using Microsoft.AspNetCore;\n\nnamespace Ocelot.Samples.Web;\n\npublic sealed class DownstreamHostBuilder : WebHostBuilder\n{\n    public static IWebHostBuilder Create() => WebHost\n        .CreateDefaultBuilder()\n        .UseDefaultServiceProvider(WithEnabledValidateScopes);\n    public static IWebHostBuilder Create(Action<ServiceProviderOptions> configure) => WebHost\n        .CreateDefaultBuilder()\n        .UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n\n    public static IWebHostBuilder Create(string[] args) => WebHost\n        .CreateDefaultBuilder(args)\n        .UseDefaultServiceProvider(WithEnabledValidateScopes);\n\n    public static IWebHostBuilder Create(string[] args, Action<ServiceProviderOptions> configure) => WebHost\n        .CreateDefaultBuilder(args)\n        .UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n\n    public static void WithEnabledValidateScopes(ServiceProviderOptions options)\n        => options.ValidateScopes = true;\n\n    // TODO Add more standard Ocelot setup\n    public static IWebHostBuilder BasicSetup() => Create(); // in CreateDefaultBuilder() implicitly calls -> .UseKestrel().UseContentRoot(Directory.GetCurrentDirectory());\n}\n"
  },
  {
    "path": "samples/Web/Ocelot.Samples.Web.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <OutputType>Library</OutputType>\n    <IsPackable>false</IsPackable>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyVersion>24.1.0</AssemblyVersion>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <ProductName>Ocelot Gateway</ProductName>\n    <Authors>Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Description>Shared library for all Ocelot samples</Description>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/samples/Web</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/Web/OcelotHostBuilder.cs",
    "content": "﻿using Microsoft.AspNetCore;\n\nnamespace Ocelot.Samples.Web;\n\npublic sealed class OcelotHostBuilder : WebHostBuilder\n{\n    public static IWebHostBuilder Create() => WebHost\n        .CreateDefaultBuilder()\n        .UseDefaultServiceProvider(WithEnabledValidateScopes);\n    public static IWebHostBuilder Create(Action<ServiceProviderOptions> configure) => WebHost\n        .CreateDefaultBuilder()\n        .UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n\n    public static IWebHostBuilder Create(string[] args) => WebHost\n        .CreateDefaultBuilder(args)\n        .UseDefaultServiceProvider(WithEnabledValidateScopes);\n\n    public static IWebHostBuilder Create(string[] args, Action<ServiceProviderOptions> configure) => WebHost\n        .CreateDefaultBuilder(args)\n        .UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n\n    public static void WithEnabledValidateScopes(ServiceProviderOptions options)\n        => options.ValidateScopes = true;\n\n    // TODO Add more standard Ocelot setup\n    public static IWebHostBuilder BasicSetup() => Create(); // in CreateDefaultBuilder() implicitly calls -> .UseKestrel().UseContentRoot(Directory.GetCurrentDirectory());\n}\n"
  },
  {
    "path": "samples/Web/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Ocelot.Samples.Web\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:60184;http://localhost:60185\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Web/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {},\n    \"net8.0\": {},\n    \"net9.0\": {}\n  }\n}"
  },
  {
    "path": "src/Ocelot/Administration/AdministrationPath.cs",
    "content": "namespace Ocelot.Administration;\n\npublic class AdministrationPath : IAdministrationPath\n{\n    public AdministrationPath(string path, string apiSecret, Uri externalJwtServerUrl = null)\n    {\n        Path = path;\n        IssuerSigningKey = apiSecret;\n        ExternalJwtSigningUrl = externalJwtServerUrl;\n    }\n\n    public string Path { get; }\n    public string IssuerSigningKey { get; }\n    public Uri ExternalJwtSigningUrl { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Administration/FileConfigurationController.cs",
    "content": "using Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Configuration.Setter;\n\nnamespace Ocelot.Administration;\n\n// [ApiController] // TODO: Make it ApiController\n[Authorize]\n[Route(\"configuration\")]\npublic class FileConfigurationController : Controller\n{\n    private readonly IFileConfigurationRepository _repo;\n    private readonly IFileConfigurationSetter _setter;\n\n    public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter)\n    {\n        _repo = repo;\n        _setter = setter;\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> Get()\n    {\n        var response = await _repo.Get();\n\n        if (response.IsError)\n        {\n            return new BadRequestObjectResult(response.Errors);\n        }\n\n        return new OkObjectResult(response.Data);\n    }\n\n    [HttpPost]\n    public async Task<IActionResult> Post([FromBody] FileConfiguration fileConfiguration)\n    {\n        try\n        {\n            var response = await _setter.Set(fileConfiguration);\n\n            if (response.IsError)\n            {\n                return new BadRequestObjectResult(response.Errors);\n            }\n\n            return new OkObjectResult(fileConfiguration);\n        }\n        catch (Exception e)\n        {\n            return new BadRequestObjectResult($\"{e.Message}:{Environment.NewLine}{e.StackTrace}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Administration/IAdministrationPath.cs",
    "content": "namespace Ocelot.Administration;\n\npublic interface IAdministrationPath\n{\n    string Path { get; }\n    string IssuerSigningKey { get; }\n    Uri ExternalJwtSigningUrl { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Administration/OutputCacheController.cs",
    "content": "using Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Ocelot.Cache;\n\nnamespace Ocelot.Administration;\n\n// [ApiController] // TODO: Make it ApiController\n//[Authorize(Policy = \"OcelotAdministration\")]\n[Authorize]\n[Route(\"outputcache\")]\npublic class OutputCacheController : Controller\n{\n    private readonly IOcelotCache<CachedResponse> _cache;\n\n    public OutputCacheController(IOcelotCache<CachedResponse> cache)\n    {\n        _cache = cache;\n    }\n\n    [HttpDelete]\n    [Route(\"{region}\")]\n    public IActionResult Delete(string region)\n    {\n        _cache.ClearRegion(region);\n        return new NoContentResult();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Authentication/AuthenticationMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Authentication;\n\npublic sealed class AuthenticationMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n\n    public AuthenticationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory)\n        : base(loggerFactory.CreateLogger<AuthenticationMiddleware>())\n    {\n        _next = next;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        var request = context.Request;\n        var path = context.Request.Path;\n        var route = context.Items.DownstreamRoute();\n\n        if (request.IsOptionsMethod() || !route.IsAuthenticated)\n        {\n            Logger.LogInformation(() => $\"No authentication is required for the path '{path}' in the route {route.Name()}.\");\n            await _next(context);\n            return;\n        }\n\n        Logger.LogInformation(() => $\"The path '{path}' is an authenticated route! {MiddlewareName} checking if client is authenticated...\");\n\n        var result = await AuthenticateAsync(context, route);\n        if (result.Principal?.Identity == null)\n        {\n            SetUnauthenticatedError(context, path, null);\n            return;\n        }\n\n        context.User = result.Principal;\n        if (context.User.Identity.IsAuthenticated)\n        {\n            Logger.LogInformation(() => $\"Client has been authenticated for path '{path}' by '{context.User.Identity.AuthenticationType}' scheme.\");\n            await _next.Invoke(context);\n            return;\n        }\n\n        SetUnauthenticatedError(context, path, context.User.Identity.Name);\n    }\n\n    private void SetUnauthenticatedError(HttpContext httpContext, string path, string userName)\n    {\n        var reason = string.IsNullOrEmpty(userName) ? \"was unauthenticated!\" : $\"by '{userName}' was unauthenticated!\";\n        var error = new UnauthenticatedError($\"Request for authenticated route '{path}' {reason}\");\n        Logger.LogWarning(() => $\"Client has NOT been authenticated for path '{path}' and pipeline error set. {error};\");\n        httpContext.Items.SetError(error);\n    }\n\n    private async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, DownstreamRoute route)\n    {\n        var notEmptySchemes = route.AuthenticationOptions.AuthenticationProviderKeys\n            .Where(s => !string.IsNullOrWhiteSpace(s));\n        AuthenticateResult result = null;\n        foreach (var scheme in notEmptySchemes)\n        {\n            try\n            {\n                result = await context.AuthenticateAsync(scheme);\n                if (result?.Succeeded == true)\n                {\n                    return result;\n                }\n            }\n            catch (Exception e)\n            {\n                Logger.LogWarning(() =>\n                    $\"Unable to authenticate the client for route '{route.Name()}' using the {scheme} authentication scheme due to error: {e.Message}\");\n            }\n        }\n\n        return result ?? AuthenticateResult.NoResult();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Authorization/AuthorizationMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Authorization;\n\npublic class AuthorizationMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IClaimsAuthorizer _claimsAuthorizer;\n    private readonly IScopesAuthorizer _scopesAuthorizer;\n\n    public AuthorizationMiddleware(RequestDelegate next,\n        IClaimsAuthorizer claimsAuthorizer,\n        IScopesAuthorizer scopesAuthorizer,\n        IOcelotLoggerFactory loggerFactory)\n        : base(loggerFactory.CreateLogger<AuthorizationMiddleware>())\n    {\n        _next = next;\n        _claimsAuthorizer = claimsAuthorizer;\n        _scopesAuthorizer = scopesAuthorizer;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        var route = context.Items.DownstreamRoute();\n\n        if (!context.IsOptionsMethod() && route.IsAuthenticated)\n        {\n            var authorized = _scopesAuthorizer.Authorize(context.User, route.AuthenticationOptions.AllowedScopes);\n            if (authorized.IsError)\n            {\n#if DEBUG\n                Logger.LogWarning(() => $\"The '{route.Name()}' route encountered authorization errors due to user scopes:{authorized.Errors.ToErrorString(true)}\");\n#endif\n                context.Items.UpsertErrors(authorized.Errors);\n                return;\n            }\n\n            if (!authorized.Data) // TODO: Looks like this is never called due to the current ScopesAuthorizer design :D Definitely a good reason to refactor\n            {\n                var error = new UnauthorizedError($\"{context.User.Identity.Name} unable to access route {route.Name()}\");\n#if DEBUG\n                Logger.LogInformation(error.ToString);\n#endif\n                context.Items.SetError(error);\n            }\n        }\n\n        if (!context.IsOptionsMethod() && route.IsAuthorized)\n        {\n            var authorized = _claimsAuthorizer.Authorize(context.User, route.RouteClaimsRequirement, context.Items.TemplatePlaceholderNameAndValues());\n            if (authorized.IsError)\n            {\n#if DEBUG\n                Logger.LogWarning(() => $\"Error whilst authorizing {context.User.Identity.Name} in route {route.Name()}:{authorized.Errors.ToErrorString(true)}\");\n#endif\n                context.Items.UpsertErrors(authorized.Errors);\n                return;\n            }\n\n            if (authorized.Data)\n            {\n#if DEBUG\n                Logger.LogInformation(() => $\"{context.User.Identity.Name} has successfully been authorized for {route.Name()}.\");\n#endif\n                await _next.Invoke(context);\n            }\n            else\n            {\n                var error = new UnauthorizedError($\"{context.User.Identity.Name} is not authorized to access '{route.Name()}' route. Setting pipeline error.\");\n#if DEBUG\n                Logger.LogInformation(error.ToString);\n#endif\n                context.Items.SetError(error);\n            }\n        }\n        else\n        {\n#if DEBUG\n            Logger.LogDebug(() => $\"No authorization needed for the route: {route.Name()}\");\n#endif\n            await _next.Invoke(context);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Authorization/ClaimValueNotAuthorizedError.cs",
    "content": "﻿using Ocelot.Errors;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic class ClaimValueNotAuthorizedError : Error\r\n{\r\n    public ClaimValueNotAuthorizedError(string message)\r\n        : base(message, OcelotErrorCode.ClaimValueNotAuthorizedError, 403)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Authorization/ClaimsAuthorizer.cs",
    "content": "﻿using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.Authorization;\n\n/// <summary>\n/// Default authorizer by claims.\n/// </summary>\npublic partial class ClaimsAuthorizer : IClaimsAuthorizer\n{\n    private readonly IClaimsParser _claimsParser;\n\n    public ClaimsAuthorizer(IClaimsParser claimsParser)\n    {\n        _claimsParser = claimsParser;\n    }\n\n    [GeneratedRegex(@\"^{(?<variable>.+)}$\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\n    private static partial Regex RegexAuthorize();\n\n    public Response<bool> Authorize(\n        ClaimsPrincipal claimsPrincipal,\n        Dictionary<string, string> routeClaimsRequirement,\n        List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues\n    )\n    {\n        foreach (var required in routeClaimsRequirement)\n        {\n            var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);\n\n            if (values.IsError)\n            {\n                return new ErrorResponse<bool>(values.Errors);\n            }\n\n            if (values.Data != null)\n            {\n                // dynamic claim\n                var match = RegexAuthorize().Match(required.Value);\n                if (match.Success)\n                {\n                    var variableName = match.Captures[0].Value;\n\n                    var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray();\n                    if (matchingPlaceholders.Length == 1)\n                    {\n                        // match\n                        var actualValue = matchingPlaceholders[0].Value;\n                        var authorized = values.Data.Contains(actualValue);\n                        if (!authorized)\n                        {\n                            return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(\n                                $\"dynamic claim value for {variableName} of {string.Join(\", \", values.Data)} is not the same as required value: {actualValue}\"));\n                        }\n                    }\n                    else\n                    {\n                        // config error\n                        if (matchingPlaceholders.Length == 0)\n                        {\n                            return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(\n                                $\"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(\", \", urlPathPlaceholderNameAndValues.Select(p => p.Name))}\"));\n                        }\n                        else\n                        {\n                            return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(\n                                $\"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(\", \", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}\"));\n                        }\n                    }\n                }\n                else\n                {\n                    // static claim\n                    var authorized = values.Data.Contains(required.Value);\n                    if (!authorized)\n                    {\n                        return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(\n                                   $\"claim value: {string.Join(\", \", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}\"));\n                    }\n                }\n            }\n            else\n            {\n                return new ErrorResponse<bool>(new UserDoesNotHaveClaimError($\"user does not have claim {required.Key}\"));\n            }\n        }\n\n        return new OkResponse<bool>(true);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Authorization/IClaimsAuthorizer.cs",
    "content": "﻿using Ocelot.DownstreamRouteFinder.UrlMatcher;\r\nusing Ocelot.Responses;\nusing System.Security.Claims;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic interface IClaimsAuthorizer\r\n{\r\n    Response<bool> Authorize(\r\n        ClaimsPrincipal claimsPrincipal,\r\n        Dictionary<string, string> routeClaimsRequirement,\r\n        List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues\r\n    );\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Authorization/IScopesAuthorizer.cs",
    "content": "﻿using Ocelot.Responses;\nusing System.Security.Claims;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic interface IScopesAuthorizer\r\n{\r\n    /// <summary>\r\n    /// Checks that the <paramref name=\"claimsPrincipal\"/> and its <see cref=\"ClaimsPrincipal.Claims\"/> collection\r\n    /// contain at least one <see cref=\"ScopeClaim\"/> value present in the <paramref name=\"routeAllowedScopes\"/> list.\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// Supports the RFC 8693 standard, allowing scope claim values as whitespace-separated strings.<br/>\r\n    /// RFC 8693 Docs: <see href=\"https://datatracker.ietf.org/doc/html/rfc8693\">OAuth 2.0 Token Exchange</see> | <see href=\"https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim\">4.2. \"scope\" (Scopes) Claim</see>.\r\n    /// </remarks>\r\n    /// <exception cref=\"ScopeNotAuthorizedError\">If not authorized.</exception>\r\n    /// <param name=\"claimsPrincipal\">Claims object from the current authentication provider's token.</param>\r\n    /// <param name=\"routeAllowedScopes\">List of allowed scopes for the route.</param>\r\n    /// <returns><see langword=\"true\"/> if any token scope claim value is in the allowed scopes; otherwise, <see langword=\"false\"/>.</returns>\r\n    Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes);\r\n\r\n    /// <summary>Gets the claim type for <c>scope</c>.</summary>\r\n    /// <value>A <see cref=\"string\"/> representing the <c>scope</c>.</value>\r\n    string ScopeClaim { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Authorization/ScopeNotAuthorizedError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Errors;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic class ScopeNotAuthorizedError : Error\r\n{\r\n    public ScopeNotAuthorizedError(string message)\r\n        : base(message, OcelotErrorCode.ScopeNotAuthorizedError, StatusCodes.Status403Forbidden)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Authorization/ScopesAuthorizer.cs",
    "content": "using Ocelot.Infrastructure.Claims;\r\nusing Ocelot.Infrastructure.Extensions;\r\nusing Ocelot.Responses;\r\nusing System.Security.Claims;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic class ScopesAuthorizer : IScopesAuthorizer\r\n{\r\n    public const string Scope = \"scope\";\r\n    public const char SpaceChar = (char)32;\r\n\r\n    private readonly IClaimsParser _claimsParser;\r\n\r\n    public ScopesAuthorizer(IClaimsParser claimsParser)\r\n    {\r\n        _claimsParser = claimsParser;\r\n    }\r\n\r\n    /// <inheritdoc/>\r\n    public string ScopeClaim => Scope;\r\n\r\n    /// <inheritdoc/>\r\n    public Response<bool> Authorize(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes)\r\n    {\r\n        if (routeAllowedScopes == null || routeAllowedScopes.Count == 0)\r\n        {\r\n            return new OkResponse<bool>(true);\r\n        }\r\n\r\n        var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, ScopeClaim);\r\n        if (values.IsError)\r\n        {\r\n            return new ErrorResponse<bool>(values.Errors);\r\n        }\r\n\r\n        IList<string> userScopes = values.Data;\r\n\r\n        // There should not be more than one scope claim that has space-separated value by design\r\n        // Some providers use array value some space-separated value but not both\r\n        // https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim\r\n        if (userScopes.Count == 1 && userScopes[0].Contains(SpaceChar))\r\n        {\r\n            userScopes = userScopes[0].Split(SpaceChar, StringSplitOptions.RemoveEmptyEntries);\r\n        }\r\n\r\n        var matchesScopes = routeAllowedScopes.Intersect(userScopes);\r\n        if (!matchesScopes.Any())\r\n        {\r\n            return new ErrorResponse<bool>(\r\n                new ScopeNotAuthorizedError($\"no one user scope: '{userScopes.Csv()}' match with some allowed scope: '{routeAllowedScopes.Csv()}'\"));\r\n        }\r\n\r\n        return new OkResponse<bool>(true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Authorization/UnauthorizedError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.Authorization;\n\npublic class UnauthorizedError : Error\n{\n    public UnauthorizedError(string message)\n        : base(message, OcelotErrorCode.UnauthorizedError, StatusCodes.Status403Forbidden)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Authorization/UserDoesNotHaveClaimError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Errors;\r\n\r\nnamespace Ocelot.Authorization;\r\n\r\npublic class UserDoesNotHaveClaimError : Error\r\n{\r\n    public UserDoesNotHaveClaimError(string message)\r\n        : base(message, OcelotErrorCode.UserDoesNotHaveClaimError, StatusCodes.Status403Forbidden)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Cache/CachedResponse.cs",
    "content": "﻿namespace Ocelot.Cache;\n\npublic class CachedResponse\n{\n    public CachedResponse(\n        HttpStatusCode statusCode,\n        Dictionary<string, IEnumerable<string>> headers,\n        string body,\n        Dictionary<string, IEnumerable<string>> contentHeaders,\n        string reasonPhrase)\n    {\n        StatusCode = statusCode;\n        Headers = headers ?? new();\n        ContentHeaders = contentHeaders ?? new();\n        Body = body ?? string.Empty;\n        ReasonPhrase = reasonPhrase;\n    }\n\n    public HttpStatusCode StatusCode { get; }\n    public Dictionary<string, IEnumerable<string>> Headers { get; }\n    public Dictionary<string, IEnumerable<string>> ContentHeaders { get; }\n    public string Body { get; }\n    public string ReasonPhrase { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/DefaultCacheKeyGenerator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.Cache;\n\npublic class DefaultCacheKeyGenerator : ICacheKeyGenerator\n{\n    public const char Delimiter = '-';\n    \n    public async ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute)\n    {\n        var builder = new StringBuilder()\n            .Append(downstreamRequest.Method)\n            .Append(Delimiter)\n            .Append(downstreamRequest.OriginalString);\n\n        var options = downstreamRoute.CacheOptions ?? new();\n        if (!string.IsNullOrEmpty(options.Header))\n        {\n            var header = downstreamRequest.Headers.TryGetValues(options.Header, out var values)\n                ? string.Join(string.Empty, values)\n                : string.Empty;\n            builder.Append(Delimiter).Append(header);\n        }\n\n        if (!options.EnableContentHashing || !downstreamRequest.HasContent)\n        {\n            return MD5Helper.GenerateMd5(builder.ToString());\n        }\n\n        var requestContent = await downstreamRequest.Request.Content.ReadAsStringAsync();\n        builder.Append(Delimiter).Append(requestContent);\n        return MD5Helper.GenerateMd5(builder.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/DefaultMemoryCache.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Cache;\n\npublic class DefaultMemoryCache<T> : IOcelotCache<T>\n{\n    private readonly IMemoryCache _memoryCache;\n    private readonly ConcurrentDictionary<string, ConcurrentBag<string>> _regions;\n\n    public DefaultMemoryCache(IMemoryCache memoryCache)\n    {\n        _memoryCache = memoryCache;\n        _regions = new();\n    }\n\n    public bool Add(string key, T value, string region, TimeSpan ttl)\n    {\n        if (ttl.TotalMilliseconds <= 0)\n        {\n            return false;\n        }\n\n        _memoryCache.Set(key, value, ttl);\n        SetRegion(region, key);\n        return true;\n    }\n\n    public T AddOrUpdate(string key, T value, string region, TimeSpan ttl)\n    {\n        if (_memoryCache.TryGetValue(key, out _))\n        {\n            _memoryCache.Remove(key);\n        }\n\n        Add(key, value, region, ttl);\n        return value;\n    }\n\n    public T Get(string key, string region)\n    {\n        if (TryGetValue(key, region, out T value))\n        {\n            return value;\n        }\n\n        return default;\n    }\n\n    public void ClearRegion(string region)\n    {\n        if (region.IsNotEmpty() && _regions.TryGetValue(region, out var keys))\n        {\n            foreach (var key in keys)\n            {\n                _memoryCache.Remove(key);\n            }\n\n            keys.Clear();\n        }\n    }\n\n    private void SetRegion(string region, string key)\n    {\n        if (region.IsNotEmpty() && _regions.TryGetValue(region, out var current))\n        {\n            if (key.IsNotEmpty() && !current.Contains(key))\n            {\n                current.Add(key);\n            }\n        }\n        else if (region.IsNotEmpty())\n        {\n            _regions.TryAdd(region, [key]);\n        }\n    }\n\n    public bool TryGetValue(string key, string region, out T value)\n    {\n        return _memoryCache.TryGetValue(key, out value);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/ICacheKeyGenerator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.Cache;\n\npublic interface ICacheKeyGenerator\n{\n    ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute);\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/IOcelotCache.cs",
    "content": "﻿namespace Ocelot.Cache;\n\npublic interface IOcelotCache<T>\n{\n    /// <summary>\n    /// Adds the specified <paramref name=\"value\"/> to the cache.\n    /// <para>Use this overload to overrule the configured expiration settings of the cache and to define a custom expiration <paramref name=\"ttl\"/> for this <paramref name=\"value\"/> only.</para>\n    /// <para>The <c>Add</c> method will <b>not</b> be successful if the specified <paramref name=\"key\"/> already exists within the cache.</para>\n    /// </summary>\n    /// <param name=\"key\">The caching key.</param>\n    /// <param name=\"value\">The <c>CacheItem</c> to be added to the cache.</param>\n    /// <param name=\"region\">The region.</param>\n    /// <param name=\"ttl\">The timeout of absolute expiration.</param>\n    /// <returns><see langword=\"true\"/> if the key was not already added to the cache, <see langword=\"false\"/> otherwise.</returns>\n    /// <exception cref=\"ArgumentNullException\">If the <paramref name=\"key\"/> or the <paramref name=\"value\"/> is <see langword=\"null\"/>.</exception>\n    bool Add(string key, T value, string region, TimeSpan ttl);\n    T AddOrUpdate(string key, T value, string region, TimeSpan ttl);\n\n    T Get(string key, string region);\n\n    void ClearRegion(string region);\n\n    bool TryGetValue(string key, string region, out T value);\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/MD5Helper.cs",
    "content": "﻿using System.Security.Cryptography;\n\nnamespace Ocelot.Cache;\n\n// TODO: Consider moving to Infrastructure\npublic static class MD5Helper\n{\n    public static string GenerateMd5(byte[] contentBytes)\n    {\n        var md5 = MD5.Create();\n        var hash = md5.ComputeHash(contentBytes);\n        var sb = new StringBuilder();\n        for (var i = 0; i < hash.Length; i++)\n        {\n            sb.Append(hash[i].ToString(\"X2\"));\n        }\n\n        return sb.ToString();\n    }\n\n    public static string GenerateMd5(string contentString)\n    {\n        var contentBytes = Encoding.Unicode.GetBytes(contentString);\n        return GenerateMd5(contentBytes);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Cache/OutputCacheMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Cache;\n\npublic class OutputCacheMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IOcelotCache<CachedResponse> _outputCache;\n    private readonly ICacheKeyGenerator _cacheGenerator;\n\n    public OutputCacheMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IOcelotCache<CachedResponse> outputCache,\n        ICacheKeyGenerator cacheGenerator)\n        : base(loggerFactory.CreateLogger<OutputCacheMiddleware>())\n    {\n        _next = next;\n        _outputCache = outputCache;\n        _cacheGenerator = cacheGenerator;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n        var options = downstreamRoute.CacheOptions;\n        if (!options.UseCache)\n        {\n            await _next.Invoke(httpContext);\n            return;\n        }\n\n        var downstreamRequest = httpContext.Items.DownstreamRequest();\n        var downstreamUrlKey = $\"{downstreamRequest.Method}-{downstreamRequest.OriginalString}\";\n        var downStreamRequestCacheKey = await _cacheGenerator.GenerateRequestCacheKey(downstreamRequest, downstreamRoute);\n\n        Logger.LogDebug(() => $\"Started checking cache for the '{downstreamUrlKey}' key.\");\n        var cached = _outputCache.Get(downStreamRequestCacheKey, options.Region);\n        if (cached != null)\n        {\n            Logger.LogDebug(() => $\"Cache entry exists for the '{downstreamUrlKey}' key.\");\n            var response = CreateHttpResponseMessage(cached);\n            SetHttpResponseMessageThisRequest(httpContext, response);\n            Logger.LogDebug(() => $\"Finished returning of cached response for the '{downstreamUrlKey}' key.\");\n            return;\n        }\n\n        Logger.LogDebug(() => $\"No response cached for the '{downstreamUrlKey}' key.\");\n\n        await _next.Invoke(httpContext);\n\n        if (httpContext.Items.Errors().Count > 0)\n        {\n            Logger.LogDebug(() => $\"There was a pipeline error for the '{downstreamUrlKey}' key.\");\n            return;\n        }\n\n        var downstreamResponse = httpContext.Items.DownstreamResponse();\n        cached = await CreateCachedResponse(downstreamResponse);\n\n        var ttl = TimeSpan.FromSeconds(options.TtlSeconds);\n        _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl);\n        Logger.LogDebug(() => $\"Finished response added to cache for the '{downstreamUrlKey}' key.\");\n    }\n\n    private static void SetHttpResponseMessageThisRequest(HttpContext context, DownstreamResponse response)\n        => context.Items.UpsertDownstreamResponse(response);\n\n    private static DownstreamResponse CreateHttpResponseMessage(CachedResponse cached)\n    {\n        var content = new MemoryStream(Convert.FromBase64String(cached.Body));\n        var streamContent = new StreamContent(content);\n        foreach (var header in cached.ContentHeaders)\n        {\n            streamContent.Headers.TryAddWithoutValidation(header.Key, header.Value);\n        }\n\n        return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList(), cached.ReasonPhrase);\n    }\n\n    private static async Task<CachedResponse> CreateCachedResponse(DownstreamResponse response)\n    {\n        if (response == null)\n        {\n            return null;\n        }\n\n        var statusCode = response.StatusCode;\n        var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values);\n        string body = null;\n\n        if (response.Content != null)\n        {\n            var content = await response.Content.ReadAsByteArrayAsync();\n            body = Convert.ToBase64String(content);\n        }\n\n        var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value);\n        var cached = new CachedResponse(statusCode, headers, body, contentHeaders, response.ReasonPhrase);\n        return cached;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Claims/AddClaimsToRequest.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.Claims;\n\npublic class AddClaimsToRequest : IAddClaimsToRequest\n{\n    private readonly IClaimsParser _claimsParser;\n\n    public AddClaimsToRequest(IClaimsParser claimsParser)\n    {\n        _claimsParser = claimsParser;\n    }\n\n    public Response SetClaimsOnContext(List<ClaimToThing> claimsToThings, HttpContext context)\n    {\n        foreach (var config in claimsToThings)\n        {\n            var value = _claimsParser.GetValue(context.User.Claims, config.NewKey, config.Delimiter, config.Index);\n\n            if (value.IsError)\n            {\n                return new ErrorResponse(value.Errors);\n            }\n\n            var exists = context.User.Claims.FirstOrDefault(x => x.Type == config.ExistingKey);\n\n            var identity = context.User.Identity as ClaimsIdentity;\n\n            if (exists != null)\n            {\n                identity?.RemoveClaim(exists);\n            }\n\n            identity?.AddClaim(new Claim(config.ExistingKey, value.Data));\n        }\n\n        return new OkResponse();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Claims/IAddClaimsToRequest.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Claims;\r\n\r\npublic interface IAddClaimsToRequest\r\n{\r\n    Response SetClaimsOnContext(List<ClaimToThing> claimsToThings,\r\n        HttpContext context);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Middleware;\r\n\r\nnamespace Ocelot.Claims.Middleware;\r\n\r\npublic class ClaimsToClaimsMiddleware : OcelotMiddleware\r\n{\r\n    private readonly RequestDelegate _next;\r\n    private readonly IAddClaimsToRequest _addClaimsToRequest;\r\n\r\n    public ClaimsToClaimsMiddleware(RequestDelegate next,\r\n        IOcelotLoggerFactory loggerFactory,\r\n        IAddClaimsToRequest addClaimsToRequest)\r\n            : base(loggerFactory.CreateLogger<ClaimsToClaimsMiddleware>())\r\n    {\r\n        _next = next;\r\n        _addClaimsToRequest = addClaimsToRequest;\r\n    }\r\n\r\n    public async Task Invoke(HttpContext httpContext)\r\n    {\r\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\r\n\r\n        if (downstreamRoute.ClaimsToClaims.Any())\r\n        {\r\n            Logger.LogDebug(\"this route has instructions to convert claims to other claims\");\r\n\r\n            var result = _addClaimsToRequest.SetClaimsOnContext(downstreamRoute.ClaimsToClaims, httpContext);\r\n\r\n            if (result.IsError)\r\n            {\r\n                Logger.LogDebug(\"error converting claims to other claims, setting pipeline error\");\r\n\r\n                httpContext.Items.UpsertErrors(result.Errors);\r\n                return;\r\n            }\r\n        }\r\n\r\n        await _next.Invoke(httpContext);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/AuthenticationOptions.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration;\n\npublic sealed class AuthenticationOptions\n{\n    public AuthenticationOptions()\n    {\n        AllowAnonymous = false;\n        AllowedScopes = new();\n        AuthenticationProviderKeys = Array.Empty<string>();\n    }\n\n    public AuthenticationOptions(FileAuthenticationOptions options)\n    {\n        AllowAnonymous = options.AllowAnonymous ?? false;\n        AllowedScopes = options.AllowedScopes ?? new();\n        AuthenticationProviderKeys = Merge(options.AuthenticationProviderKey, options.AuthenticationProviderKeys ?? Array.Empty<string>());\n    }\n\n    public AuthenticationOptions(List<string> allowedScopes, string[] authenticationProviderKeys)\n    {\n        AllowAnonymous = false;\n        AllowedScopes = allowedScopes ?? new();\n        AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty<string>();\n    }\n\n    private static string[] Merge(string primaryKey, string[] keys)\n    {\n        if (primaryKey.IsEmpty())\n        {\n            return keys;\n        }\n\n        List<string> merged = new(1 + keys.Length) { primaryKey };\n        merged.AddRange(keys);\n        return merged.ToArray();\n    }\n\n    /// <summary>Allows anonymous authentication for route when global authentication options are used.</summary>\n    /// <value><see langword=\"true\"/> if it is allowed; otherwise, <see langword=\"false\"/>.</value>\n    public bool AllowAnonymous { get; init; }\n    public List<string> AllowedScopes { get; init; }\n\n    /// <summary>Multiple authentication schemes registered in DI services with appropriate authentication providers.</summary>\n    /// <remarks>The order in the collection matters: first successful authentication result wins.</remarks>\n    /// <value>An array of <see langword=\"string\"/> values of the scheme names.</value>\n    public string[] AuthenticationProviderKeys { get; init; }\n\n    public bool HasScheme => AuthenticationProviderKeys.Any(k => !k.IsEmpty());\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs",
    "content": "﻿using Ocelot.Configuration.Creator;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Values;\n\nnamespace Ocelot.Configuration.Builder;\n\npublic class DownstreamRouteBuilder\n{\n    private AuthenticationOptions _authenticationOptions;\n    private string _loadBalancerKey;\n    private string _downstreamPathTemplate;\n    private UpstreamPathTemplate _upstreamTemplatePattern;\n    private HashSet<HttpMethod> _upstreamHttpMethod;\n    private List<ClaimToThing> _claimsToHeaders;\n    private List<ClaimToThing> _claimToClaims;\n    private Dictionary<string, string> _routeClaimRequirement;\n    private List<ClaimToThing> _claimToQueries;\n    private List<ClaimToThing> _claimToDownstreamPath;\n    private string _requestIdHeaderKey;\n    private CacheOptions _cacheOptions;\n    private string _downstreamScheme;\n    private LoadBalancerOptions _loadBalancerOptions;\n    private QoSOptions _qosOptions;\n    private HttpHandlerOptions _httpHandlerOptions;\n    private RateLimitOptions _rateLimitOptions;\n    private string _serviceName;\n    private string _serviceNamespace;\n    private List<HeaderFindAndReplace> _upstreamHeaderFindAndReplace;\n    private List<HeaderFindAndReplace> _downstreamHeaderFindAndReplace;\n    private readonly List<DownstreamHostAndPort> _downstreamAddresses;\n    private string _key;\n    private List<string> _delegatingHandlers;\n    private List<AddHeader> _addHeadersToDownstream;\n    private List<AddHeader> _addHeadersToUpstream;\n    private bool _dangerousAcceptAnyServerCertificateValidator;\n    private SecurityOptions _securityOptions;\n    private string _downstreamHttpMethod;\n    private Version _downstreamHttpVersion;\n    private HttpVersionPolicy _downstreamHttpVersionPolicy;\n    private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;\n    private MetadataOptions _metadataOptions;\n    private int? _timeout;\n\n    public DownstreamRouteBuilder()\n    {\n        _downstreamAddresses = new();\n        _delegatingHandlers = new();\n        _addHeadersToDownstream = new();\n        _addHeadersToUpstream = new();\n    }\n\n    public DownstreamRouteBuilder WithDownstreamAddresses(List<DownstreamHostAndPort> downstreamAddresses)\n    {\n        _downstreamAddresses.AddRange(downstreamAddresses);\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownStreamHttpMethod(string method)\n    {\n        _downstreamHttpMethod = method;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions)\n    {\n        _loadBalancerOptions = loadBalancerOptions;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownstreamScheme(string downstreamScheme)\n    {\n        _downstreamScheme = downstreamScheme;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownstreamPathTemplate(string input)\n    {\n        _downstreamPathTemplate = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input)\n    {\n        _upstreamTemplatePattern = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithUpstreamHttpMethod(IEnumerable<string> methods)\n    {\n        _upstreamHttpMethod = methods.ToHttpMethods();\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithRequestIdKey(string input)\n    {\n        _requestIdHeaderKey = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithClaimsToHeaders(List<ClaimToThing> input)\n    {\n        _claimsToHeaders = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithClaimsToClaims(List<ClaimToThing> input)\n    {\n        _claimToClaims = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithRouteClaimsRequirement(Dictionary<string, string> input)\n    {\n        _routeClaimRequirement = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithClaimsToQueries(List<ClaimToThing> input)\n    {\n        _claimToQueries = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithClaimsToDownstreamPath(List<ClaimToThing> input)\n    {\n        _claimToDownstreamPath = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithCacheOptions(CacheOptions input)\n    {\n        _cacheOptions = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithQosOptions(QoSOptions input)\n    {\n        _qosOptions = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithLoadBalancerKey(string loadBalancerKey)\n    {\n        _loadBalancerKey = loadBalancerKey;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions)\n    {\n        _authenticationOptions = authenticationOptions;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithRateLimitOptions(RateLimitOptions input)\n    {\n        _rateLimitOptions = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithHttpHandlerOptions(HttpHandlerOptions input)\n    {\n        _httpHandlerOptions = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithServiceName(string serviceName)\n    {\n        _serviceName = serviceName;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithServiceNamespace(string serviceNamespace)\n    {\n        _serviceNamespace = serviceNamespace;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithUpstreamHeaderFindAndReplace(List<HeaderFindAndReplace> upstreamHeaderFindAndReplace)\n    {\n        _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownstreamHeaderFindAndReplace(List<HeaderFindAndReplace> downstreamHeaderFindAndReplace)\n    {\n        _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithKey(string key)\n    {\n        _key = key;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDelegatingHandlers(List<string> delegatingHandlers)\n    {\n        _delegatingHandlers = delegatingHandlers;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithAddHeadersToDownstream(List<AddHeader> addHeadersToDownstream)\n    {\n        _addHeadersToDownstream = addHeadersToDownstream;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithAddHeadersToUpstream(List<AddHeader> addHeadersToUpstream)\n    {\n        _addHeadersToUpstream = addHeadersToUpstream;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDangerousAcceptAnyServerCertificateValidator(bool dangerousAcceptAnyServerCertificateValidator)\n    {\n        _dangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithSecurityOptions(SecurityOptions securityOptions)\n    {\n        _securityOptions = securityOptions;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVersion)\n    {\n        _downstreamHttpVersion = downstreamHttpVersion;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary<string, UpstreamHeaderTemplate> input)\n    {\n        _upstreamHeaders = input;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy downstreamHttpVersionPolicy)\n    {\n        _downstreamHttpVersionPolicy = downstreamHttpVersionPolicy;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions)\n    {\n        _metadataOptions = metadataOptions;\n        return this;\n    }\n\n    public DownstreamRouteBuilder WithTimeout(int? timeout)\n    {\n        _timeout = timeout;\n        return this;\n    }\n\n    public DownstreamRoute Build()\n    {\n        return new DownstreamRoute(\n            _key,\n            _upstreamTemplatePattern,\n            _upstreamHeaderFindAndReplace,\n            _downstreamHeaderFindAndReplace,\n            _downstreamAddresses,\n            _serviceName,\n            _serviceNamespace,\n            _httpHandlerOptions,\n            _qosOptions,\n            _downstreamScheme,\n            _requestIdHeaderKey,\n            _cacheOptions,\n            _loadBalancerOptions,\n            _rateLimitOptions,\n            _routeClaimRequirement,\n            _claimToQueries,\n            _claimsToHeaders,\n            _claimToClaims,\n            _claimToDownstreamPath,\n            _authenticationOptions,\n            new DownstreamPathTemplate(_downstreamPathTemplate),\n            _loadBalancerKey,\n            _delegatingHandlers,\n            _addHeadersToDownstream,\n            _addHeadersToUpstream,\n            _dangerousAcceptAnyServerCertificateValidator,\n            _securityOptions,\n            _downstreamHttpMethod,\n            _downstreamHttpVersion,\n            _downstreamHttpVersionPolicy,\n            _upstreamHeaders,\n            _metadataOptions,\n            _timeout);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs",
    "content": "﻿using System.Globalization;\n\nnamespace Ocelot.Configuration.Builder;\n\npublic class MetadataOptionsBuilder\n{\n    private string[] _separators;\n    private char[] _trimChars;\n    private StringSplitOptions _stringSplitOption;\n    private NumberStyles _numberStyle;\n    private CultureInfo _currentCulture;\n    private IDictionary<string, string> _metadata;\n\n    public MetadataOptionsBuilder WithSeparators(string[] separators)\n    {\n        _separators = separators;\n        return this;\n    }\n\n    public MetadataOptionsBuilder WithTrimChars(char[] trimChars)\n    {\n        _trimChars = trimChars;\n        return this;\n    }\n\n    public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption)\n    {\n        _stringSplitOption = Enum.Parse<StringSplitOptions>(stringSplitOption);\n        return this;\n    }\n\n    public MetadataOptionsBuilder WithNumberStyle(string numberStyle)\n    {\n        _numberStyle = Enum.Parse<NumberStyles>(numberStyle);\n        return this;\n    }\n\n    public MetadataOptionsBuilder WithCurrentCulture(string currentCulture)\n    {\n        _currentCulture = CultureInfo.GetCultureInfo(currentCulture);\n        return this;\n    }\n\n    public MetadataOptionsBuilder WithMetadata(IDictionary<string, string> metadata)\n    {\n        _metadata = metadata;\n        return this;\n    }\n\n    public MetadataOptions Build()\n    {\n        return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs",
    "content": "namespace Ocelot.Configuration.Builder;\n\npublic class ServiceProviderConfigurationBuilder\n{\n    private string _serviceDiscoveryProviderScheme;\n    private string _serviceDiscoveryProviderHost;\n    private int _serviceDiscoveryProviderPort;\n    private string _type;\n    private string _token;\n    private string _configurationKey;\n    private int _pollingInterval;\n    private string _namespace;\n\n    public ServiceProviderConfigurationBuilder WithScheme(string serviceDiscoveryProviderScheme)\n    {\n        _serviceDiscoveryProviderScheme = serviceDiscoveryProviderScheme;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost)\n    {\n        _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithPort(int serviceDiscoveryProviderPort)\n    {\n        _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithType(string type)\n    {\n        _type = type;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithToken(string token)\n    {\n        _token = token;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithConfigurationKey(string configurationKey)\n    {\n        _configurationKey = configurationKey;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithPollingInterval(int pollingInterval)\n    {\n        _pollingInterval = pollingInterval;\n        return this;\n    }\n\n    public ServiceProviderConfigurationBuilder WithNamespace(string @namespace)\n    {\n        _namespace = @namespace;\n        return this;\n    }\n\n    public ServiceProviderConfiguration Build() => new()\n    {\n        ConfigurationKey = _configurationKey,\n        Host = _serviceDiscoveryProviderHost,\n        Namespace = _namespace,\n        PollingInterval = _pollingInterval,\n        Port = _serviceDiscoveryProviderPort,\n        Scheme = _serviceDiscoveryProviderScheme,\n        Token = _token,\n        Type = _type,\n    };\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs",
    "content": "﻿using Ocelot.Values;\n\nnamespace Ocelot.Configuration.Builder;\n\npublic class UpstreamPathTemplateBuilder\n{\n    private string _template;\n    private int _priority;\n    private bool _containsQueryString;\n    private string _originalValue;\n\n    public UpstreamPathTemplateBuilder WithTemplate(string template)\n    {\n        _template = template;\n        return this;\n    }\n\n    public UpstreamPathTemplateBuilder WithPriority(int priority)\n    {\n        _priority = priority;\n        return this;\n    }\n\n    public UpstreamPathTemplateBuilder WithContainsQueryString(bool containsQueryString)\n    {\n        _containsQueryString = containsQueryString;\n        return this;\n    }\n\n    public UpstreamPathTemplateBuilder WithOriginalValue(string originalValue)\n    {\n        _originalValue = originalValue;\n        return this;\n    }\n\n    public UpstreamPathTemplate Build()\n    {\n        return new UpstreamPathTemplate(_template, _priority, _containsQueryString, _originalValue);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/CacheOptions.cs",
    "content": "﻿using Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure.Extensions;\r\nusing Ocelot.Request.Middleware;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\npublic class CacheOptions\r\n{\r\n    public const int NoSeconds = 0;\r\n\r\n    /// <summary>\r\n    /// Separation of concerns between Ocelot's native caching control and the industry-standard <c>Cache-Control</c> header, which governs downstream caching behavior.\r\n    /// </summary>\r\n    public const string Oc_Cache_Control = \"OC-Cache-Control\";\r\n\r\n    internal CacheOptions() { }\r\n    public CacheOptions(FileCacheOptions from, string defaultRegion)\r\n        : this(from.TtlSeconds, from.Region.IfEmpty(defaultRegion), from.Header, from.EnableContentHashing)\r\n    { }\r\n\r\n    /// <summary>\r\n    /// Initializes a new instance of the <see cref=\"CacheOptions\"/> class.\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// Internal defaults:\r\n    ///   <list type=\"bullet\">\r\n    ///   <item>The default value for <see cref=\"EnableContentHashing\"/> is <see langword=\"false\"/>, but it is set to null for route-level configuration to allow global configuration usage.</item>\r\n    ///   <item>The default value for <see cref=\"TtlSeconds\"/> is 0.</item>\r\n    ///   </list>\r\n    /// </remarks>\r\n    /// <param name=\"ttlSeconds\">Time-to-live seconds. If not speciefied, zero value is used by default.</param>\r\n    /// <param name=\"region\">The region of caching.</param>\r\n    /// <param name=\"header\">The header name to control cached value.</param>\r\n    /// <param name=\"enableContentHashing\">The switcher for content hashing. If not speciefied, false value is used by default.</param>\n    public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing)\r\n    {\r\n        TtlSeconds = ttlSeconds ?? NoSeconds;\r\n        Region = region;\n        Header = header.IfEmpty(Oc_Cache_Control);\r\n        EnableContentHashing = enableContentHashing ?? false;\r\n    }\r\n\r\n    /// <summary>Time-to-live seconds.</summary>\r\n    /// <remarks>Default value is 0. No caching by default.</remarks>\r\n    /// <value>An <see cref=\"int\"/> value of seconds.</value>\r\n    public int TtlSeconds { get; }\r\n    public string Region { get; }\n    public string Header { get; }\n\n    /// <summary>Enables MD5 hash calculation of the <see cref=\"HttpRequestMessage.Content\"/> of the <see cref=\"DownstreamRequest.Request\"/> object.</summary>\n    /// <remarks>Default value is <see langword=\"false\"/>. No hashing by default.</remarks>\r\n    /// <value><see langword=\"true\"/> if hashing is enabled, otherwise it is <see langword=\"false\"/>.</value>\n    public bool EnableContentHashing { get; }\r\n\r\n    public bool UseCache => TtlSeconds > NoSeconds;\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/ChangeTracking/IOcelotConfigurationChangeTokenSource.cs",
    "content": "using Microsoft.Extensions.Primitives;\r\n\r\nnamespace Ocelot.Configuration.ChangeTracking;\r\n\r\n/// <summary>\r\n/// <see cref=\"IChangeToken\" /> source which is activated when Ocelot's configuration is changed.\r\n/// </summary>\r\npublic interface IOcelotConfigurationChangeTokenSource\r\n{\r\n    IChangeToken ChangeToken { get; }\r\n\r\n    void Activate();\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeToken.cs",
    "content": "using Microsoft.Extensions.Primitives;\n\nnamespace Ocelot.Configuration.ChangeTracking;\n\npublic class OcelotConfigurationChangeToken : IChangeToken\n{\n    public const double PollingIntervalSeconds = 1;\n\n    private readonly ICollection<CallbackWrapper> _callbacks = new List<CallbackWrapper>();\n    private readonly object _lock = new();\n    private DateTime? _timeChanged;\n\n    public IDisposable RegisterChangeCallback(Action<object> callback, object state)\n    {\n        lock (_lock)\n        {\n            var wrapper = new CallbackWrapper(callback, state, _callbacks, _lock);\n            _callbacks.Add(wrapper);\n            return wrapper;\n        }\n    }\n\n    public void Activate()\n    {\n        lock (_lock)\n        {\n            _timeChanged = DateTime.UtcNow;\n            foreach (var wrapper in _callbacks)\n            {\n                wrapper.Invoke();\n            }\n        }\n    }\n\n    // Token stays active for PollingIntervalSeconds after a change (could be parameterised) - otherwise HasChanged would be true forever.\n    // Taking suggestions for better ways to reset HasChanged back to false.\n    public bool HasChanged => _timeChanged.HasValue && (DateTime.UtcNow - _timeChanged.Value).TotalSeconds < PollingIntervalSeconds;\n\n    public bool ActiveChangeCallbacks => true;\n\n    private class CallbackWrapper : IDisposable\n    {\n        private readonly ICollection<CallbackWrapper> _callbacks;\n        private readonly object _lock;\n\n        public CallbackWrapper(Action<object> callback, object state, ICollection<CallbackWrapper> callbacks, object @lock)\n        {\n            _callbacks = callbacks;\n            _lock = @lock;\n            Callback = callback;\n            State = state;\n        }\n\n        public void Invoke()\n        {\n            Callback.Invoke(State);\n        }\n\n        public void Dispose()\n        {\n            lock (_lock)\n            {\n                _callbacks.Remove(this);\n            }\n        }\n\n        public Action<object> Callback { get; }\n\n        public object State { get; }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSource.cs",
    "content": "using Microsoft.Extensions.Primitives;\r\n\r\nnamespace Ocelot.Configuration.ChangeTracking;\r\n\r\npublic class OcelotConfigurationChangeTokenSource : IOcelotConfigurationChangeTokenSource\r\n{\r\n    private readonly OcelotConfigurationChangeToken _changeToken = new();\r\n\r\n    public IChangeToken ChangeToken => _changeToken;\r\n\r\n    public void Activate()\r\n    {\r\n        _changeToken.Activate();\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationMonitor.cs",
    "content": "using Microsoft.Extensions.Options;\nusing Ocelot.Configuration.Repository;\n\nnamespace Ocelot.Configuration.ChangeTracking;\n\npublic class OcelotConfigurationMonitor : IOptionsMonitor<IInternalConfiguration>\n{\n    private readonly IInternalConfigurationRepository _repo;\n    private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;\n\n    public OcelotConfigurationMonitor(IInternalConfigurationRepository repo, IOcelotConfigurationChangeTokenSource changeTokenSource)\n    {\n        ArgumentNullException.ThrowIfNull(repo);\n        ArgumentNullException.ThrowIfNull(changeTokenSource);\n        _repo = repo;\n        _changeTokenSource = changeTokenSource;\n    }\n\n    public IInternalConfiguration Get(string name)\n    {\n        return _repo.Get().Data;\n    }\n\n    public IDisposable OnChange(Action<IInternalConfiguration, string> listener)\n    {\n        ArgumentNullException.ThrowIfNull(listener);\n        return _changeTokenSource.ChangeToken.RegisterChangeCallback(_ => listener(CurrentValue, string.Empty), null);\n    }\n\n    public IInternalConfiguration CurrentValue => _repo.Get().Data;\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/ClaimToThing.cs",
    "content": "﻿namespace Ocelot.Configuration;\r\n\r\npublic class ClaimToThing\r\n{\r\n    public ClaimToThing(string existingKey, string newKey, string delimiter, int index)\r\n    {\r\n        NewKey = newKey;\r\n        Delimiter = delimiter;\r\n        Index = index;\r\n        ExistingKey = existingKey;\r\n    }\r\n\r\n    public string ExistingKey { get; }\r\n    public string NewKey { get; }\r\n    public string Delimiter { get; }\r\n    public int Index { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/AddHeader.cs",
    "content": "namespace Ocelot.Configuration.Creator;\n\npublic class AddHeader\n{\n    public AddHeader(string key, string value)\n    {\n        Key = key;\n        Value = value;\n    }\n\n    public string Key { get; }\n    public string Value { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/AggregatesCreator.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class AggregatesCreator : IAggregatesCreator\r\n{\r\n    private readonly IUpstreamTemplatePatternCreator _creator;\n    private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator;\r\n\r\n    public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator)\r\n    {\r\n        _creator = creator;\n        _headerCreator = headerCreator;\r\n    }\r\n\r\n    public List<Route> Create(FileConfiguration fileConfiguration, IReadOnlyList<Route> routes)\r\n    {\r\n        return fileConfiguration.Aggregates\r\n            .Select(aggregate => SetUpAggregateRoute(routes, aggregate, fileConfiguration.GlobalConfiguration))\r\n            .Where(aggregate => aggregate != null)\r\n            .ToList();\r\n    }\r\n\r\n    private Route SetUpAggregateRoute(IEnumerable<Route> routes, FileAggregateRoute aggregateRoute, FileGlobalConfiguration globalConfiguration)\r\n    {\r\n        var applicableRoutes = new List<DownstreamRoute>();\r\n        var allRoutes = routes.SelectMany(x => x.DownstreamRoute);\r\n        var downstreamRoutes = aggregateRoute.RouteKeys.Select(routeKey => allRoutes.FirstOrDefault(q => q.Key == routeKey));\r\n        foreach (var downstreamRoute in downstreamRoutes)\n        {\n            if (downstreamRoute == null)\n            {\n                return null;\n            }\n\n            applicableRoutes.Add(downstreamRoute);\n        }\r\n\r\n        var upstreamTemplatePattern = _creator.Create(aggregateRoute);\n        var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute);\r\n        var upstreamHttpMethod = aggregateRoute.UpstreamHttpMethod.ToHttpMethods();\r\n        return new Route()\r\n        {\r\n            Aggregator = aggregateRoute.Aggregator,\r\n            DownstreamRoute = applicableRoutes,\r\n            DownstreamRouteConfig = aggregateRoute.RouteKeysConfig,\r\n            UpstreamHeaderTemplates = upstreamHeaderTemplates,\r\n            UpstreamHost = aggregateRoute.UpstreamHost,\r\n            UpstreamHttpMethod = upstreamHttpMethod,\r\n            UpstreamTemplatePattern = upstreamTemplatePattern,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure.Extensions;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class AuthenticationOptionsCreator : IAuthenticationOptionsCreator\r\n{\r\n    public AuthenticationOptions Create(FileAuthenticationOptions options)\r\n        => new(options);\r\n\r\n    public AuthenticationOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(route);\r\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\r\n        return Create(route, route.AuthenticationOptions, globalConfiguration.AuthenticationOptions);\r\n    }\r\n\r\n    public AuthenticationOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(route);\r\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\r\n        return Create(route, route.AuthenticationOptions, globalConfiguration.AuthenticationOptions);\r\n    }\r\n\r\n    protected virtual AuthenticationOptions Create(IRouteGrouping grouping, FileAuthenticationOptions options, FileGlobalAuthenticationOptions globalOptions)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(grouping);\r\n\r\n        bool isGlobal = globalOptions?.RouteKeys is null // undefined section or array option -> is global\r\n            || globalOptions.RouteKeys.Count == 0 // empty collection -> is global\r\n            || globalOptions.RouteKeys.Contains(grouping.Key); // this route is in the group\r\n\r\n        if (options == null && globalOptions != null && isGlobal)\r\n        {\r\n            return new(globalOptions);\r\n        }\r\n\r\n        if (options != null && globalOptions == null)\r\n        {\r\n            return new(options);\r\n        }\r\n\r\n        if (options != null && globalOptions != null)\r\n        {\r\n            return isGlobal ? Merge(options, globalOptions) : new(options);\r\n        }\r\n\r\n        return new();\r\n    }\r\n\r\n    protected virtual AuthenticationOptions Merge(FileAuthenticationOptions options, FileAuthenticationOptions globalOptions)\r\n    {\r\n        options.AllowAnonymous ??= globalOptions.AllowAnonymous;\r\n        options.AllowedScopes ??= globalOptions.AllowedScopes;\r\n        options.AuthenticationProviderKey = options.AuthenticationProviderKey.IfEmpty(globalOptions.AuthenticationProviderKey);\r\n        if (!(options.AuthenticationProviderKeys?.Length > 0)) // TODO IfEmpty ICollection\r\n        {\r\n            options.AuthenticationProviderKeys = globalOptions.AuthenticationProviderKeys ?? [];\r\n        }\r\n\r\n        return new(options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class CacheOptionsCreator : ICacheOptionsCreator\n{\n    public CacheOptions Create(FileCacheOptions options)\n        => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing);\n\n    public CacheOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.FileCacheOptions ?? route.CacheOptions, globalConfiguration.CacheOptions, loadBalancingKey);\n    }\n\n    public CacheOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.CacheOptions, globalConfiguration.CacheOptions, loadBalancingKey);\n    }\n\n    protected virtual CacheOptions Create(IRouteGrouping grouping, FileCacheOptions options, FileGlobalCacheOptions globalOptions, string loadBalancingKey)\n    {\n        ArgumentNullException.ThrowIfNull(grouping);\n        var group = globalOptions;\n        var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global\n            group.RouteKeys.Count == 0 || // empty collection -> is global\n            group.RouteKeys.Contains(grouping.Key); // this route is in the group\n\n        if (options == null && globalOptions != null && isGlobal)\n        {\n            return new(globalOptions, loadBalancingKey);\n        }\n\n        if (options != null && globalOptions == null)\n        {\n            return new(options, loadBalancingKey);\n        }\n        else if (options != null && globalOptions != null && !isGlobal)\n        {\n            return new(options, loadBalancingKey);\n        }\n\n        if (options != null && globalOptions != null && isGlobal)\n        {\n            return Merge(options, globalOptions, loadBalancingKey);\n        }\n\n        return new();\n    }\n\n    protected virtual CacheOptions Merge(FileCacheOptions options, FileCacheOptions globalOptions, string defaultRegion)\n    {\n        var region = options.Region.IfEmpty(globalOptions.Region).IfEmpty(defaultRegion);\n        var header = options.Header.IfEmpty(globalOptions.Header).IfEmpty(CacheOptions.Oc_Cache_Control);\n        var ttlSeconds = options.TtlSeconds ?? globalOptions.TtlSeconds;\n        var enableHashing = options.EnableContentHashing ?? globalOptions.EnableContentHashing;\n        return new CacheOptions(ttlSeconds, region, header, enableHashing);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs",
    "content": "using Ocelot.Configuration.Parser;\r\nusing Ocelot.Logging;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class ClaimsToThingCreator : IClaimsToThingCreator\r\n{\r\n    private readonly IClaimToThingConfigurationParser _claimToThingConfigParser;\r\n    private readonly IOcelotLogger _logger;\r\n\r\n    public ClaimsToThingCreator(IClaimToThingConfigurationParser claimToThingConfigurationParser,\r\n        IOcelotLoggerFactory loggerFactory)\r\n    {\r\n        _logger = loggerFactory.CreateLogger<ClaimsToThingCreator>();\r\n        _claimToThingConfigParser = claimToThingConfigurationParser;\r\n    }\n\n    public List<ClaimToThing> Create(Dictionary<string, string> inputToBeParsed)\r\n    {\r\n        var claimsToThings = new List<ClaimToThing>();\r\n\r\n        foreach (var input in inputToBeParsed)\r\n        {\r\n            var claimToThing = _claimToThingConfigParser.Extract(input.Key, input.Value);\r\n\r\n            if (claimToThing.IsError)\r\n            {\r\n                _logger.LogDebug(() => $\"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect\");\r\n            }\r\n            else\r\n            {\r\n                claimsToThings.Add(claimToThing.Data);\r\n            }\r\n        }\r\n\r\n        return claimsToThings;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ConfigurationCreator.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Administration;\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class ConfigurationCreator : IConfigurationCreator\r\n{\r\n    private readonly IAuthenticationOptionsCreator _authOptionsCreator;\r\n    private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator;\r\n    private readonly IQoSOptionsCreator _qosOptionsCreator;\r\n    private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;\r\n    private readonly IAdministrationPath _adminPath;\r\n    private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator;\r\n    private readonly IVersionCreator _versionCreator;\r\n    private readonly IVersionPolicyCreator _versionPolicyCreator;\n    private readonly IMetadataCreator _metadataCreator;\r\n    private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator;\r\n    private readonly ICacheOptionsCreator _cacheOptionsCreator;\r\n\r\n    public ConfigurationCreator(\r\n        IServiceProvider serviceProvider,\r\n        IAuthenticationOptionsCreator authOptionsCreator,\r\n        IServiceProviderConfigurationCreator serviceProviderConfigCreator,\r\n        IQoSOptionsCreator qosOptionsCreator,\r\n        IHttpHandlerOptionsCreator httpHandlerOptionsCreator,\r\n        ILoadBalancerOptionsCreator loadBalancerOptionsCreator,\r\n        IVersionCreator versionCreator,\r\n        IVersionPolicyCreator versionPolicyCreator,\r\n        IMetadataCreator metadataCreator,\r\n        IRateLimitOptionsCreator rateLimitOptionsCreator,\r\n        ICacheOptionsCreator cacheOptionsCreator)\r\n    {\r\n        _adminPath = serviceProvider.GetService<IAdministrationPath>();\r\n        _authOptionsCreator = authOptionsCreator;\r\n        _loadBalancerOptionsCreator = loadBalancerOptionsCreator;\r\n        _serviceProviderConfigCreator = serviceProviderConfigCreator;\r\n        _qosOptionsCreator = qosOptionsCreator;\r\n        _httpHandlerOptionsCreator = httpHandlerOptionsCreator;\r\n        _versionCreator = versionCreator;\r\n        _versionPolicyCreator = versionPolicyCreator;\r\n        _metadataCreator = metadataCreator;\r\n        _rateLimitOptionsCreator = rateLimitOptionsCreator;\r\n        _cacheOptionsCreator = cacheOptionsCreator;\n    }\r\n\r\n    public InternalConfiguration Create(FileConfiguration configuration, Route[] routes)\r\n    {\r\n        var adminPath = _adminPath?.Path;\r\n        var globalConfiguration = configuration.GlobalConfiguration ?? new();\r\n        var authOptions = _authOptionsCreator.Create(globalConfiguration.AuthenticationOptions);\r\n        var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(globalConfiguration);\r\n        var lbOptions = _loadBalancerOptionsCreator.Create(globalConfiguration.LoadBalancerOptions);\r\n        var qosOptions = _qosOptionsCreator.Create(globalConfiguration.QoSOptions);\r\n        var httpHandlerOptions = _httpHandlerOptionsCreator.Create(globalConfiguration.HttpHandlerOptions);\r\n        var version = _versionCreator.Create(globalConfiguration.DownstreamHttpVersion);\r\n        var versionPolicy = _versionPolicyCreator.Create(globalConfiguration.DownstreamHttpVersionPolicy);\n        var metadataOptions = _metadataCreator.Create(null, globalConfiguration);\r\n        var rateLimitOptions = _rateLimitOptionsCreator.Create(globalConfiguration);\r\n        var cacheOptions = _cacheOptionsCreator.Create(globalConfiguration.CacheOptions);\r\n\r\n        return new InternalConfiguration(routes)\r\n        {\r\n            AdministrationPath = adminPath,\r\n            AuthenticationOptions = authOptions,\r\n            CacheOptions = cacheOptions,\r\n            DownstreamHttpVersion = version,\r\n            DownstreamHttpVersionPolicy = versionPolicy,\r\n            DownstreamScheme = globalConfiguration.DownstreamScheme,\r\n            HttpHandlerOptions = httpHandlerOptions,\r\n            LoadBalancerOptions = lbOptions,\r\n            MetadataOptions = metadataOptions,\r\n            QoSOptions = qosOptions,\r\n            RateLimitOptions = rateLimitOptions,\r\n            RequestId = globalConfiguration.RequestIdKey,\r\n            ServiceProviderConfiguration = serviceProviderConfiguration,\r\n            Timeout = globalConfiguration.Timeout,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs",
    "content": "﻿using Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// This class implements the <see cref=\"IMetadataCreator\"/> interface.\n/// </summary>\npublic class DefaultMetadataCreator : IMetadataCreator\n{\n    public MetadataOptions Create(IDictionary<string, string> metadata, FileGlobalConfiguration globalConfiguration)\n    {\n        metadata ??= new Dictionary<string, string>();\n        globalConfiguration.Metadata ??= new Dictionary<string, string>();\n        var merged = new Dictionary<string, string>(globalConfiguration.Metadata);\n        foreach (var (key, value) in metadata)\n        {\n            merged[key] = value;\n        }\n\n        var options = globalConfiguration.MetadataOptions;\n        return new MetadataOptionsBuilder()\n            .WithMetadata(merged)\n            .WithSeparators(options.Separators)\n            .WithTrimChars(options.TrimChars)\n            .WithStringSplitOption(options.StringSplitOption)\n            .WithNumberStyle(options.NumberStyle)\n            .WithCurrentCulture(options.CurrentCulture)\n            .Build();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class DownstreamAddressesCreator : IDownstreamAddressesCreator\r\n{\r\n    public List<DownstreamHostAndPort> Create(FileRoute route)\r\n    {\r\n        return route.DownstreamHostAndPorts.Select(hostAndPort => new DownstreamHostAndPort(hostAndPort.Host, hostAndPort.Port)).ToList();\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/DynamicRoutesCreator.cs",
    "content": "using Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class DynamicRoutesCreator : IDynamicsCreator\n{\n    private readonly IAuthenticationOptionsCreator _authOptionsCreator;\n    private readonly ICacheOptionsCreator _cacheOptionsCreator;\n    private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;\n    private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator;\n    private readonly IMetadataCreator _metadataCreator;\n    private readonly IQoSOptionsCreator _qosOptionsCreator;\n    private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator;\n    private readonly IRouteKeyCreator _loadBalancerKeyCreator;\n    private readonly IVersionCreator _versionCreator;\n    private readonly IVersionPolicyCreator _versionPolicyCreator;\n\n    public DynamicRoutesCreator(\n        IAuthenticationOptionsCreator authOptionsCreator,\n        ICacheOptionsCreator cacheOptionsCreator,\n        IHttpHandlerOptionsCreator handlerOptionsCreator,\n        ILoadBalancerOptionsCreator loadBalancerOptionsCreator,\n        IMetadataCreator metadataCreator,\n        IQoSOptionsCreator qosOptionsCreator,\n        IRateLimitOptionsCreator rateLimitOptionsCreator,\n        IRouteKeyCreator loadBalancerKeyCreator,\n        IVersionCreator versionCreator,\n        IVersionPolicyCreator versionPolicyCreator)\n    {\n        _authOptionsCreator = authOptionsCreator;\n        _cacheOptionsCreator = cacheOptionsCreator;\n        _httpHandlerOptionsCreator = handlerOptionsCreator;\n        _loadBalancerKeyCreator = loadBalancerKeyCreator;\n        _loadBalancerOptionsCreator = loadBalancerOptionsCreator;\n        _metadataCreator = metadataCreator;\n        _qosOptionsCreator = qosOptionsCreator;\n        _rateLimitOptionsCreator = rateLimitOptionsCreator;\n        _versionCreator = versionCreator;\n        _versionPolicyCreator = versionPolicyCreator;\n    }\n\n    public IReadOnlyList<Route> Create(FileConfiguration fileConfiguration)\n    {\n        Route CreateRoute(FileDynamicRoute route)\n            => SetUpDynamicRoute(route, fileConfiguration.GlobalConfiguration);\n        return fileConfiguration.DynamicRoutes\n            .Select(CreateRoute)\n            .ToArray();\n    }\n\n    public virtual int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global)\n    {\n        int def = DownstreamRoute.DefaultTimeoutSeconds;\n        return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def;\n    }\n\n    private Route SetUpDynamicRoute(FileDynamicRoute dynamicRoute, FileGlobalConfiguration globalConfiguration)\n    {\n        // The old RateLimitRule property takes precedence over the new RateLimitOptions property for backward compatibility, thus, override forcibly\n        if (dynamicRoute.RateLimitRule != null)\n        {\n            dynamicRoute.RateLimitOptions = dynamicRoute.RateLimitRule;\n        }\n\n        // Load balancing dependants\n        var lbOptions = _loadBalancerOptionsCreator.Create(dynamicRoute, globalConfiguration);\n        var lbKey = _loadBalancerKeyCreator.Create(dynamicRoute, lbOptions);\n        var cacheOptions = _cacheOptionsCreator.Create(dynamicRoute, globalConfiguration, lbKey);\n\n        var authOptions = _authOptionsCreator.Create(dynamicRoute, globalConfiguration);\n        var version = _versionCreator.Create(dynamicRoute.DownstreamHttpVersion.IfEmpty(globalConfiguration.DownstreamHttpVersion));\n        var versionPolicy = _versionPolicyCreator.Create(dynamicRoute.DownstreamHttpVersionPolicy.IfEmpty(globalConfiguration.DownstreamHttpVersionPolicy));\n        var scheme = dynamicRoute.DownstreamScheme.IfEmpty(globalConfiguration.DownstreamScheme);\n        var handlerOptions = _httpHandlerOptionsCreator.Create(dynamicRoute, globalConfiguration);\n        var metadata = _metadataCreator.Create(dynamicRoute.Metadata, globalConfiguration);\n        var qosOptions = _qosOptionsCreator.Create(dynamicRoute, globalConfiguration);\n        var rlOptions = _rateLimitOptionsCreator.Create(dynamicRoute, globalConfiguration);\n        var timeout = CreateTimeout(dynamicRoute, globalConfiguration);\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(authOptions)\n            .WithCacheOptions(cacheOptions)\n            .WithDownstreamHttpVersion(version)\n            .WithDownstreamHttpVersionPolicy(versionPolicy)\n            .WithDownstreamScheme(scheme)\n            .WithHttpHandlerOptions(handlerOptions)\n            .WithLoadBalancerKey(lbKey)\n            .WithLoadBalancerOptions(lbOptions)\n            .WithMetadata(metadata)\n            .WithQosOptions(qosOptions)\n            .WithRateLimitOptions(rlOptions)\n            .WithServiceName(dynamicRoute.ServiceName)\n            .WithServiceNamespace(dynamicRoute.ServiceNamespace)\n            .WithTimeout(timeout)\n            .Build();\n        return new Route(true, downstreamRoute); // IsDynamic -> true\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class FileInternalConfigurationCreator : IInternalConfigurationCreator\r\n{\r\n    private readonly IConfigurationValidator _configurationValidator;\r\n    private readonly IConfigurationCreator _configCreator;\r\n    private readonly IDynamicsCreator _dynamicsCreator;\r\n    private readonly IRoutesCreator _routesCreator;\r\n    private readonly IAggregatesCreator _aggregatesCreator;\r\n\r\n    public FileInternalConfigurationCreator(\r\n        IConfigurationValidator configurationValidator,\r\n        IRoutesCreator routesCreator,\r\n        IAggregatesCreator aggregatesCreator,\r\n        IDynamicsCreator dynamicsCreator,\r\n        IConfigurationCreator configCreator\r\n        )\r\n    {\r\n        _configCreator = configCreator;\r\n        _dynamicsCreator = dynamicsCreator;\r\n        _aggregatesCreator = aggregatesCreator;\r\n        _routesCreator = routesCreator;\r\n        _configurationValidator = configurationValidator;\r\n    }\r\n\r\n    public async Task<Response<IInternalConfiguration>> Create(FileConfiguration fileConfiguration)\r\n    {\r\n        var response = await _configurationValidator.IsValid(fileConfiguration);\r\n\r\n        if (response.Data.IsError)\r\n        {\r\n            return new ErrorResponse<IInternalConfiguration>(response.Data.Errors);\r\n        }\r\n\r\n        var routes = _routesCreator.Create(fileConfiguration);\r\n\r\n        var aggregates = _aggregatesCreator.Create(fileConfiguration, routes);\r\n\r\n        var dynamicRoute = _dynamicsCreator.Create(fileConfiguration);\r\n\r\n        var mergedRoutes = routes\r\n            .Union(aggregates)\r\n            .Union(dynamicRoute)\r\n            .ToArray();\r\n\r\n        var config = _configCreator.Create(fileConfiguration, mergedRoutes);\r\n\r\n        return new OkResponse<IInternalConfiguration>(config);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs",
    "content": "using Microsoft.Extensions.Options;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure;\r\nusing Ocelot.Infrastructure.Extensions;\r\nusing Ocelot.Logging;\r\nusing Header = System.Collections.Generic.KeyValuePair<string, string>;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator\r\n{\r\n    private readonly IPlaceholders _placeholders;\r\n    private readonly IOcelotLogger _logger;\r\n    private readonly FileGlobalConfiguration _globalConfiguration;\r\n\r\n    public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory, IOptions<FileGlobalConfiguration> global)\r\n    {\r\n        _placeholders = placeholders;\r\n        _logger = factory.CreateLogger<HeaderFindAndReplaceCreator>();\r\n        _globalConfiguration = global.Value;\r\n    }\r\n\r\n    public HeaderTransformations Create(FileRoute route)\r\n        => Create(route, _globalConfiguration);\r\n\r\n    public HeaderTransformations Create(FileRoute route, FileGlobalConfiguration global)\r\n    {\r\n        global ??= _globalConfiguration;\r\n        var upstreamTransform = Merge(route.UpstreamHeaderTransform, global.UpstreamHeaderTransform);\r\n        var (upstream, addHeadersToUpstream) = ProcessHeaders(upstreamTransform, nameof(route.UpstreamHeaderTransform));\r\n\r\n        var downstreamTransform = Merge(route.DownstreamHeaderTransform, global.DownstreamHeaderTransform);\r\n        var (downstream, addHeadersToDownstream) = ProcessHeaders(downstreamTransform, nameof(route.DownstreamHeaderTransform));\r\n        \r\n        return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream);\r\n    }\r\n\r\n    /// <summary>Merge global Up/Downstream settings to the Route local ones.</summary>\r\n    /// <param name=\"local\">The Route local settings.</param>\r\n    /// <param name=\"global\">Global default settings.</param>\r\n    /// <returns> An <see cref=\"IEnumerable{T}\"/> collection where T is <see cref=\"Header\"/>.</returns>\r\n    public static IEnumerable<Header> Merge(IDictionary<string, string> local, IDictionary<string, string> global)\r\n    {\r\n        local ??= new Dictionary<string, string>();\r\n        global ??= new Dictionary<string, string>();\r\n        var toAdd = global.ExceptBy(local.Keys, x => x.Key); // Winning strategy: the route Transform-value wins over global one\r\n        return local.Union(toAdd);\r\n    }\r\n\r\n    private (List<HeaderFindAndReplace> StreamHeaders, List<AddHeader> AddHeaders)\r\n        ProcessHeaders(IEnumerable<Header> headerTransform, string propertyName)\r\n    {\r\n        var addHeaders = new List<AddHeader>();\r\n        var streamHeaders = new List<HeaderFindAndReplace>();\r\n\r\n        foreach (var input in headerTransform)\r\n        {\r\n            if (input.Value.Contains(HeaderFindAndReplace.Comma))\r\n            {\r\n                var hAndr = Map(input);\r\n                if (hAndr != null)\r\n                {\r\n                    streamHeaders.Add(hAndr);\r\n                }\r\n                else\r\n                {\r\n                    _logger.LogWarning(() => $\"Unable to add {propertyName} {input}\");\r\n                }\r\n            }\r\n            else\r\n            {\r\n                addHeaders.Add(new AddHeader(input.Key, input.Value));\r\n            }\r\n        }\r\n\r\n        return (streamHeaders, addHeaders);\r\n    }\r\n\r\n    private HeaderFindAndReplace Map(Header input)\r\n    {\r\n        var findAndReplace = input.Value.Split(HeaderFindAndReplace.Comma);\r\n        var replace = findAndReplace[1].TrimStart();\r\n\r\n        var startOfPlaceholder = replace.IndexOf(Placeholders.OpeningBrace, StringComparison.Ordinal);\r\n        if (startOfPlaceholder > -1)\r\n        {\r\n            var endOfPlaceholder = replace.IndexOf(Placeholders.ClosingBrace, startOfPlaceholder);\r\n            var placeholder = replace.Substring(startOfPlaceholder,\r\n                                                endOfPlaceholder - startOfPlaceholder + 1);\r\n            var value = _placeholders.Get(placeholder);\r\n            if (value.IsError)\r\n            {\r\n                _logger.LogWarning(() => $\"{nameof(HeaderFindAndReplace)} was not mapped from {input} due to {value.Errors.ToErrorString()}\");\r\n                return null;\r\n            }\r\n\r\n            replace = replace.Replace(placeholder, value.Data);\r\n        }\r\n\r\n        return new(input.Key, findAndReplace[0], replace, 0);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/HeaderTransformations.cs",
    "content": "namespace Ocelot.Configuration.Creator;\r\n\r\npublic class HeaderTransformations\r\n{\r\n    public HeaderTransformations(\r\n        List<HeaderFindAndReplace> upstream,\n        List<HeaderFindAndReplace> downstream,\r\n        List<AddHeader> addHeaderToDownstream,\r\n        List<AddHeader> addHeaderToUpstream)\r\n    {\r\n        AddHeadersToDownstream = addHeaderToDownstream;\r\n        AddHeadersToUpstream = addHeaderToUpstream;\r\n        Upstream = upstream;\r\n        Downstream = downstream;\r\n    }\r\n\r\n    public List<HeaderFindAndReplace> Upstream { get; }\r\n\r\n    public List<HeaderFindAndReplace> Downstream { get; }\r\n\r\n    public List<AddHeader> AddHeadersToDownstream { get; }\r\n    public List<AddHeader> AddHeadersToUpstream { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator\n{\n    private readonly IOcelotTracer _tracer;\n    public HttpHandlerOptionsCreator(IServiceProvider services)\n        => _tracer = services.GetService<IOcelotTracer>();\n\n    public HttpHandlerOptions Create(FileHttpHandlerOptions options)\n    {\n        options ??= new();\n        var hasTracer = _tracer != null;\n        return new(options, hasTracer);\n    }\n\n    public HttpHandlerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.HttpHandlerOptions, globalConfiguration.HttpHandlerOptions);\n    }\n\n    public HttpHandlerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.HttpHandlerOptions, globalConfiguration.HttpHandlerOptions);\n    }\n\n    protected virtual HttpHandlerOptions Create(IRouteGrouping grouping, FileHttpHandlerOptions options, FileGlobalHttpHandlerOptions globalOptions)\n    {\n        ArgumentNullException.ThrowIfNull(grouping);\n        var group = globalOptions;\n        var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global\n            group.RouteKeys.Count == 0 || // empty collection -> is global\n            group.RouteKeys.Contains(grouping.Key); // this route is in the group\n        var hasTracer = _tracer != null;\n        if (options == null && globalOptions != null && isGlobal)\n        {\n            return new(globalOptions, hasTracer);\n        }\n\n        if (options != null && globalOptions == null)\n        {\n            return new(options, hasTracer);\n        }\n        else if (options != null && globalOptions != null && !isGlobal)\n        {\n            return new(options, hasTracer);\n        }\n\n        if (options != null && globalOptions != null && isGlobal)\n        {\n            return Merge(options, globalOptions);\n        }\n\n        return new();\n    }\n\n    protected virtual HttpHandlerOptions Merge(FileHttpHandlerOptions options, FileHttpHandlerOptions globalOptions)\n    {\n        ArgumentNullException.ThrowIfNull(options);\n        ArgumentNullException.ThrowIfNull(globalOptions);\n        options.AllowAutoRedirect ??= globalOptions.AllowAutoRedirect ?? false;\n        options.MaxConnectionsPerServer ??= globalOptions.MaxConnectionsPerServer ?? int.MaxValue;\n        options.PooledConnectionLifetimeSeconds ??= globalOptions.PooledConnectionLifetimeSeconds ?? HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds;\n        options.UseCookieContainer ??= globalOptions.UseCookieContainer ?? false;\n        options.UseProxy ??= globalOptions.UseProxy ?? false;\n        options.UseTracing ??= globalOptions.UseTracing ?? false;\n        var useTracing = _tracer != null && options.UseTracing.Value;\n        return new(options, useTracing);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/HttpVersionCreator.cs",
    "content": "﻿namespace Ocelot.Configuration.Creator;\n\npublic class HttpVersionCreator : IVersionCreator\n{\n    public Version Create(string downstreamHttpVersion)\n    {\n        if (!Version.TryParse(downstreamHttpVersion, out var version))\n        {\n            version = new Version(1, 1);\n        }\n\n        return version;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs",
    "content": "﻿namespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// Default implementation of the <see cref=\"IVersionPolicyCreator\"/> interface.\n/// </summary>\npublic class HttpVersionPolicyCreator : IVersionPolicyCreator\n{\n    /// <summary>\n    /// Creates a <see cref=\"HttpVersionPolicy\"/> by a string.\n    /// </summary>\n    /// <param name=\"downstreamHttpVersionPolicy\">The string representation of the version policy.</param>\n    /// <returns>An <see cref=\"HttpVersionPolicy\"/> enumeration value.</returns>\n    public HttpVersionPolicy Create(string downstreamHttpVersionPolicy) => downstreamHttpVersionPolicy switch\n    {\n        VersionPolicies.RequestVersionExact => HttpVersionPolicy.RequestVersionExact,\n        VersionPolicies.RequestVersionOrHigher => HttpVersionPolicy.RequestVersionOrHigher,\n        VersionPolicies.RequestVersionOrLower => HttpVersionPolicy.RequestVersionOrLower,\n        _ => HttpVersionPolicy.RequestVersionOrLower,\n    };\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IAggregatesCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IAggregatesCreator\r\n{\r\n    List<Route> Create(FileConfiguration fileConfiguration, IReadOnlyList<Route> routes);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IAuthenticationOptionsCreator\r\n{\r\n    AuthenticationOptions Create(FileAuthenticationOptions options);\r\n    AuthenticationOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration);\r\n    AuthenticationOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// This interface is used to create cache options.\n/// </summary>\npublic interface ICacheOptionsCreator\n{\n    CacheOptions Create(FileCacheOptions options);\n    CacheOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey);\n    CacheOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IClaimsToThingCreator.cs",
    "content": "namespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IClaimsToThingCreator\r\n{\r\n    List<ClaimToThing> Create(Dictionary<string, string> thingsBeingAdded);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IConfigurationCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IConfigurationCreator\r\n{\r\n    InternalConfiguration Create(FileConfiguration configuration, Route[] routes);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IDownstreamAddressesCreator\r\n{\r\n    List<DownstreamHostAndPort> Create(FileRoute route);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IDynamicsCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IDynamicsCreator\r\n{\r\n    IReadOnlyList<Route> Create(FileConfiguration fileConfiguration);\r\n\r\n    /// <summary>\r\n    /// Creates a timeout value for a given file route based on the global configuration.\r\n    /// </summary>\r\n    /// <param name=\"route\">The file route for which to create the timeout.</param>\r\n    /// <param name=\"global\">The global configuration to use for creating the timeout.</param>\r\n    /// <returns>The timeout value in seconds.</returns>\r\n    int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IHeaderFindAndReplaceCreator\r\n{\r\n    HeaderTransformations Create(FileRoute fileRoute);\r\n    HeaderTransformations Create(FileRoute route, FileGlobalConfiguration globalConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IHttpHandlerOptionsCreator\r\n{\r\n    HttpHandlerOptions Create(FileHttpHandlerOptions options);\r\n    HttpHandlerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration);\r\n    HttpHandlerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface IInternalConfigurationCreator\n{\n    Task<Response<IInternalConfiguration>> Create(FileConfiguration fileConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface ILoadBalancerOptionsCreator\n{\n    LoadBalancerOptions Create(FileLoadBalancerOptions options);\n    LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration);\n    LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IMetadataCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// This interface describes the creation of metadata options.\n/// </summary>\npublic interface IMetadataCreator\n{\n    MetadataOptions Create(IDictionary<string, string> metadata, FileGlobalConfiguration globalConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface IQoSOptionsCreator\n{\n    QoSOptions Create(FileQoSOptions options);\n    QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration);\n    QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface IRateLimitOptionsCreator\n{\n    RateLimitOptions Create(FileGlobalConfiguration globalConfiguration);\n    RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IRequestIdKeyCreator\r\n{\r\n    string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IRouteKeyCreator\r\n{\r\n    string Create(FileRoute route, LoadBalancerOptions loadBalancing);\r\n    string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing);\r\n    string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IRoutesCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface IRoutesCreator\n{\n    IReadOnlyList<Route> Create(FileConfiguration fileConfiguration);\n    \n    /// <summary>\n    /// Creates a timeout value for a given file route based on the global configuration.\n    /// </summary>\n    /// <param name=\"route\">The file route for which to create the timeout.</param>\n    /// <param name=\"global\">The global configuration to use for creating the timeout.</param>\n    /// <returns>The timeout value in seconds.</returns>\n    int CreateTimeout(FileRoute route, FileGlobalConfiguration global);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ISecurityOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic interface ISecurityOptionsCreator\n{\n    SecurityOptions Create(FileSecurityOptions securityOptions, FileGlobalConfiguration global);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IServiceProviderConfigurationCreator\r\n{\r\n    ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Values;\n\nnamespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers\">Routing based on request header</see>.\n/// </summary>\npublic interface IUpstreamHeaderTemplatePatternCreator\n{\n    /// <summary>\n    /// Creates upstream templates based on route headers.\n    /// </summary>\n    /// <param name=\"route\">The route info.</param>\n    /// <returns>An <see cref=\"IDictionary{TKey, TValue}\"/> object where TKey is <see langword=\"string\"/>, TValue is <see cref=\"UpstreamHeaderTemplate\"/>.</returns>\n    IDictionary<string, UpstreamHeaderTemplate> Create(IRouteUpstream route);\n\n    IDictionary<string, UpstreamHeaderTemplate> Create(IHeaderDictionary upstreamHeaderTemplates, bool routeIsCaseSensitive);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\nusing Ocelot.Values;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic interface IUpstreamTemplatePatternCreator\r\n{\r\n    UpstreamPathTemplate Create(IRouteUpstream route);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IVersionCreator.cs",
    "content": "﻿namespace Ocelot.Configuration.Creator;\n\npublic interface IVersionCreator\n{\n    Version Create(string downstreamHttpVersion);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs",
    "content": "﻿namespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// Defines conversions from version policy strings to <see cref=\"HttpVersionPolicy\"/> enumeration values.\n/// </summary>\npublic interface IVersionPolicyCreator\n{\n    /// <summary>\n    /// Creates a <see cref=\"HttpVersionPolicy\"/> by a string.\n    /// </summary>\n    /// <param name=\"downstreamHttpVersionPolicy\">The string representation of the version policy.</param>\n    /// <returns>An <see cref=\"HttpVersionPolicy\"/> enumeration value.</returns>\n    HttpVersionPolicy Create(string downstreamHttpVersionPolicy);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class LoadBalancerOptionsCreator : ILoadBalancerOptionsCreator\n{\n    public LoadBalancerOptions Create(FileLoadBalancerOptions options)\n        => new(options);\n\n    public LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions);\n    }\n\n    public LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions);\n    }\n\n    protected virtual LoadBalancerOptions Create(IRouteGrouping grouping, FileLoadBalancerOptions options, FileGlobalLoadBalancerOptions globalOptions)\n    {\n        ArgumentNullException.ThrowIfNull(grouping);\n        var group = globalOptions;\n        var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global\n            group.RouteKeys.Count == 0 || // empty collection -> is global\n            group.RouteKeys.Contains(grouping.Key); // this route is in the group\n\n        if (options == null && globalOptions != null && isGlobal)\n        {\n            return new(globalOptions);\n        }\n\n        if (options != null && globalOptions == null)\n        {\n            return new(options);\n        }\n        else if (options != null && globalOptions != null && !isGlobal)\n        {\n            return new(options);\n        }\n\n        if (options != null && globalOptions != null && isGlobal)\n        {\n            return Merge(options, globalOptions);\n        }\n\n        return new();\n    }\n\n    protected virtual LoadBalancerOptions Merge(FileLoadBalancerOptions options, FileLoadBalancerOptions globalOptions)\n    {\n        ArgumentNullException.ThrowIfNull(options);\n        ArgumentNullException.ThrowIfNull(globalOptions);\n        options.Type = options.Type.IfEmpty(globalOptions.Type);\n        options.Key = options.Key.IfEmpty(globalOptions.Key);\n        options.Expiry ??= globalOptions.Expiry;\n        return new(options);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs",
    "content": "using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class QoSOptionsCreator : IQoSOptionsCreator\n{\n    public QoSOptions Create(FileQoSOptions options)\n        => new(options ?? new());\n\n    public QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.QoSOptions, globalConfiguration.QoSOptions);\n    }\n\n    public QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n        return Create(route, route.QoSOptions, globalConfiguration.QoSOptions);\n    }\n\n    protected virtual QoSOptions Create(IRouteGrouping grouping, FileQoSOptions options, FileGlobalQoSOptions globalOptions)\n    {\n        ArgumentNullException.ThrowIfNull(grouping);\n\n        bool isGlobal = globalOptions?.RouteKeys is null // undefined section or array option -> is global\n            || globalOptions.RouteKeys.Count == 0 // empty collection -> is global\n            || globalOptions.RouteKeys.Contains(grouping.Key); // this route is in the group\n\n        if (options == null && globalOptions != null && isGlobal)\n        {\n            return new(globalOptions);\n        }\n\n        if (options != null && globalOptions == null)\n        {\n            return new(options);\n        }\n\n        if (options != null && globalOptions != null)\n        {\n            return isGlobal ? Merge(options, globalOptions) : new(options);\n        }\n\n        return new();\n    }\n\n    protected virtual QoSOptions Merge(FileQoSOptions options, FileQoSOptions global)\n    {\n        options ??= new();\n        global ??= new();\n        options.DurationOfBreak ??= global.DurationOfBreak;\n        options.BreakDuration ??= global.BreakDuration;\n        options.ExceptionsAllowedBeforeBreaking ??= global.ExceptionsAllowedBeforeBreaking;\n        options.MinimumThroughput ??= global.MinimumThroughput;\n        options.FailureRatio ??= global.FailureRatio;\n        options.SamplingDuration ??= global.SamplingDuration;\n        options.TimeoutValue ??= global.TimeoutValue;\n        options.Timeout ??= global.Timeout;\n        return new(options);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class RateLimitOptionsCreator : IRateLimitOptionsCreator\n{\n    public RateLimitOptionsCreator() { }\n\n    public RateLimitOptions Create(FileGlobalConfiguration globalConfiguration)\n        => globalConfiguration.RateLimitOptions != null\n            ? new(globalConfiguration.RateLimitOptions)\n            : new(false);\n\n    public RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(globalConfiguration);\n\n        var rule = route.RateLimitOptions;\n        var globalOptions = globalConfiguration.RateLimitOptions;\n        var group = globalOptions as IRouteGroup;\n        var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global\n            group.RouteKeys.Count == 0 || // empty collection -> is global\n            group.RouteKeys.Contains(route.Key); // this route is in the group\n\n        if (rule?.EnableRateLimiting == false || (isGlobal && globalOptions?.EnableRateLimiting == false))\n        {\n            return new(false);\n        }\n\n        // By Client's Header rule merging\n        if (rule == null && globalOptions != null && isGlobal)\n        {\n            return new(globalOptions);\n        }\n\n        if (rule != null && (globalOptions == null || (globalOptions != null && !isGlobal)))\n        {\n            return new(rule);\n        }\n\n        if (rule != null && globalOptions != null && isGlobal)\n        {\n            return MergeHeaderRules(rule, globalOptions);\n        }\n\n        return new(false);\n    }\n\n    protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule rule, FileRateLimitByHeaderRule globalRule)\n    {\n        ArgumentNullException.ThrowIfNull(rule);\n        ArgumentNullException.ThrowIfNull(globalRule);\n\n        rule.ClientIdHeader = rule.ClientIdHeader.IfEmpty(globalRule.ClientIdHeader.IfEmpty(RateLimitOptions.DefaultClientHeader));\n        rule.ClientWhitelist ??= globalRule.ClientWhitelist ?? [];\n        if (!(rule.ClientWhitelist?.Count > 0)) // TODO IfEmpty ICollection\n        {\n            rule.ClientWhitelist = globalRule.ClientWhitelist ?? [];\n        }\n\n        // Final merging of EnableHeaders is implemented in the constructor\n        rule.DisableRateLimitHeaders ??= globalRule.DisableRateLimitHeaders;\n        rule.EnableHeaders ??= globalRule.EnableHeaders;\n\n        rule.EnableRateLimiting ??= globalRule.EnableRateLimiting ?? true;\n\n        // Final merging of StatusCode is implemented in the constructor\n        rule.HttpStatusCode ??= globalRule.HttpStatusCode;\n        rule.StatusCode ??= globalRule.StatusCode;\n\n        // Final merging of QuotaMessage is implemented in the constructor\n        rule.QuotaExceededMessage = rule.QuotaExceededMessage.IfEmpty(globalRule.QuotaExceededMessage);\n        rule.QuotaMessage = rule.QuotaMessage.IfEmpty(globalRule.QuotaMessage);\n\n        // Final merging of KeyPrefix is implemented in the constructor\n        rule.RateLimitCounterPrefix = rule.RateLimitCounterPrefix.IfEmpty(globalRule.RateLimitCounterPrefix);\n        rule.KeyPrefix = rule.KeyPrefix.IfEmpty(globalRule.KeyPrefix);\n\n        rule.Period = rule.Period.IfEmpty(globalRule.Period.IfEmpty(RateLimitRule.DefaultPeriod));\n\n        // Final merging of Wait is implemented in the constructor\n        rule.PeriodTimespan ??= globalRule.PeriodTimespan;\n        rule.Wait = rule.Wait.IfEmpty(globalRule.Wait.IfEmpty(RateLimitRule.ZeroWait));\n\n        rule.Limit ??= globalRule.Limit ?? RateLimitRule.ZeroLimit;\n        return new(rule);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class RequestIdKeyCreator : IRequestIdKeyCreator\r\n{\r\n    public string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration)\r\n    {\r\n        var routeId = !string.IsNullOrEmpty(fileRoute.RequestIdKey);\r\n\r\n        var requestIdKey = routeId\r\n           ? fileRoute.RequestIdKey\r\n           : globalConfiguration.RequestIdKey;\r\n\r\n        return requestIdKey;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/RouteKeyCreator.cs",
    "content": "using Ocelot.Configuration.File;\r\nusing Ocelot.DownstreamRouteFinder.Finder;\r\nusing Ocelot.Infrastructure.Extensions;\r\nusing Ocelot.LoadBalancer.Balancers;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class RouteKeyCreator : IRouteKeyCreator\r\n{\r\n    public const char Separator = '|';\r\n    public const char Dot = DiscoveryDownstreamRouteFinder.Dot;\r\n\r\n    /// <summary>\n    /// Creates the unique <see langword=\"string\"/> key based on the route properties for load balancing etc.\n    /// </summary>\n    /// <remarks>\n    /// Key template: <c>UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey</c>.\n    /// </remarks>\r\n    /// <param name=\"route\">The route object.</param>\r\n    /// <param name=\"loadBalancing\">Final options for load balancing.</param>\n    /// <returns>A <see langword=\"string\"/> object containing the key.</returns>\n    public string Create(FileRoute route, LoadBalancerOptions loadBalancing)\r\n    {\r\n        if (TryStickySession(loadBalancing, out var stickySessionKey))\r\n        {\r\n            return stickySessionKey;\r\n        }\r\n\r\n        var keyBuilder = new StringBuilder()\n            .AppendNext(route.UpstreamHttpMethod.Csv()) // required\r\n            .AppendNext(route.UpstreamPathTemplate) // required\r\n            .AppendNext(route.UpstreamHost.IfEmpty(\"no-host\")) // optional...\r\n            .AppendNext(route.DownstreamHostAndPorts.Select(AsString).Csv().IfEmpty(\"no-host-and-port\"))\r\n            .AppendNext(route.ServiceNamespace.IfEmpty(\"no-svc-ns\"))\r\n            .AppendNext(route.ServiceName.IfEmpty(\"no-svc-name\"))\r\n            .AppendNext(loadBalancing.Type.IfEmpty(\"no-lb-type\"))\r\n            .AppendNext(loadBalancing.Key.IfEmpty(\"no-lb-key\"));\r\n        return keyBuilder.ToString();\r\n    }\r\n\r\n    public string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing)\r\n    {\r\n        if (TryStickySession(loadBalancing, out var stickySessionKey))\r\n        {\r\n            return stickySessionKey;\r\n        }\r\n\r\n        // it should be constructed in upper contexts\r\n        return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key\r\n            : Create(route.ServiceNamespace, route.ServiceName, loadBalancing);\r\n    }\r\n\r\n    public string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing)\r\n    {\r\n        if (TryStickySession(loadBalancing, out var stickySessionKey))\r\n        {\r\n            return stickySessionKey;\r\n        }\r\n\r\n        return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key\r\n            : string.Join(Dot, serviceNamespace, serviceName); // upstreamHttpMethod ?\r\n    }\r\n\r\n    protected virtual bool TryStickySession(LoadBalancerOptions loadBalancing, out string stickySessionKey)\r\n    {\r\n        bool isStickySession = nameof(CookieStickySessions).Equals(loadBalancing.Type, StringComparison.OrdinalIgnoreCase)\r\n            && loadBalancing.Key.Length > 0;\r\n        stickySessionKey = isStickySession\r\n            ? $\"{nameof(CookieStickySessions)}:{loadBalancing.Key}\"\r\n            : string.Empty;\r\n        return isStickySession;\r\n    }\r\n\r\n    public static string AsString(FileHostAndPort host) => host?.ToString();\r\n}\n\ninternal static class RouteKeyCreatorHelpers\n{\r\n    /// <summary>Helper function to append a string to the key builder, separated by a pipe.</summary>\n    /// <param name=\"builder\">The builder of the key.</param>\n    /// <param name=\"next\">The next word to add.</param>\r\n    /// <param name=\"separator\">The character used to separate entries.</param>\n    /// <returns>The reference to the builder.</returns>\n    public static StringBuilder AppendNext(this StringBuilder builder, string next, char separator = RouteKeyCreator.Separator)\n        => StringBuilderExtensions.AppendNext(builder, next, separator);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/SecurityOptionsCreator.cs",
    "content": "﻿using NetTools; // <PackageReference Include=\"IPAddressRange\" Version=\"6.0.0\" />\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class SecurityOptionsCreator : ISecurityOptionsCreator\n{\n    public SecurityOptions Create(FileSecurityOptions securityOptions, FileGlobalConfiguration global)\n    {\n        var options = securityOptions.IsEmpty() ? global.SecurityOptions : securityOptions;\n        var allowedIPs = options.IPAllowedList.SelectMany(Parse)\n            .ToArray();\n        var blockedIPs = options.IPBlockedList.SelectMany(Parse)\n            .Except(options.ExcludeAllowedFromBlocked ? allowedIPs : Enumerable.Empty<string>())\n            .ToArray();\n        return new(allowedIPs, blockedIPs);\n    }\n\n    private static string[] Parse(string ipValue)\n    {\n        if (IPAddressRange.TryParse(ipValue, out var range))\n        {\n            return range.Select<IPAddress, string>(ip => ip.ToString()).ToArray();\n        }\n\n        return Array.Empty<string>();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs",
    "content": "using Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class ServiceProviderConfigurationCreator : IServiceProviderConfigurationCreator\n{\n    public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration)\n    {\n        var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;\n        var scheme = globalConfiguration?.ServiceDiscoveryProvider?.Scheme ?? \"http\";\n        var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? \"localhost\";\n        var type = !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Type)\n            ? globalConfiguration?.ServiceDiscoveryProvider?.Type\n            : \"consul\";\n        var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0;\n        var k8snamespace = globalConfiguration?.ServiceDiscoveryProvider?.Namespace ?? string.Empty;\n\n        return new ServiceProviderConfigurationBuilder()\n            .WithScheme(scheme)\n            .WithHost(host)\n            .WithPort(port)\n            .WithType(type)\n            .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token)\n            .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey)\n            .WithPollingInterval(pollingInterval)\n            .WithNamespace(k8snamespace)\n            .Build();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs",
    "content": "using Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Creator;\n\npublic class StaticRoutesCreator : IRoutesCreator\n{\n    private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator;\n    private readonly IClaimsToThingCreator _claimsToThingCreator;\n    private readonly IAuthenticationOptionsCreator _authOptionsCreator;\n    private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator;\n    private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator;\n    private readonly IRequestIdKeyCreator _requestIdKeyCreator;\n    private readonly IQoSOptionsCreator _qosOptionsCreator;\n    private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator;\n    private readonly ICacheOptionsCreator _cacheOptionsCreator;\n    private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;\n    private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator;\n    private readonly IDownstreamAddressesCreator _downstreamAddressesCreator;\n    private readonly IRouteKeyCreator _routeKeyCreator;\n    private readonly ISecurityOptionsCreator _securityOptionsCreator;\n    private readonly IVersionCreator _versionCreator;\n    private readonly IVersionPolicyCreator _versionPolicyCreator;\n    private readonly IMetadataCreator _metadataCreator;\n\n    public StaticRoutesCreator(\n        IClaimsToThingCreator claimsToThingCreator,\n        IAuthenticationOptionsCreator authOptionsCreator,\n        IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator,\n        IRequestIdKeyCreator requestIdKeyCreator,\n        IQoSOptionsCreator qosOptionsCreator,\n        IRateLimitOptionsCreator rateLimitOptionsCreator,\n        ICacheOptionsCreator cacheOptionsCreator,\n        IHttpHandlerOptionsCreator httpHandlerOptionsCreator,\n        IHeaderFindAndReplaceCreator headerFAndRCreator,\n        IDownstreamAddressesCreator downstreamAddressesCreator,\n        ILoadBalancerOptionsCreator loadBalancerOptionsCreator,\n        IRouteKeyCreator routeKeyCreator,\n        ISecurityOptionsCreator securityOptionsCreator,\n        IVersionCreator versionCreator,\n        IVersionPolicyCreator versionPolicyCreator,\n        IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator,\n        IMetadataCreator metadataCreator)\n    {\n        _routeKeyCreator = routeKeyCreator;\n        _loadBalancerOptionsCreator = loadBalancerOptionsCreator;\n        _downstreamAddressesCreator = downstreamAddressesCreator;\n        _headerFAndRCreator = headerFAndRCreator;\n        _cacheOptionsCreator = cacheOptionsCreator;\n        _rateLimitOptionsCreator = rateLimitOptionsCreator;\n        _requestIdKeyCreator = requestIdKeyCreator;\n        _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator;\n        _authOptionsCreator = authOptionsCreator;\n        _claimsToThingCreator = claimsToThingCreator;\n        _qosOptionsCreator = qosOptionsCreator;\n        _httpHandlerOptionsCreator = httpHandlerOptionsCreator;\n        _loadBalancerOptionsCreator = loadBalancerOptionsCreator;\n        _securityOptionsCreator = securityOptionsCreator;\n        _versionCreator = versionCreator;\n        _versionPolicyCreator = versionPolicyCreator;\n        _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;\n        _metadataCreator = metadataCreator;\n    }\n\n    public IReadOnlyList<Route> Create(FileConfiguration fileConfiguration)\n    {\n        Route CreateRoute(FileRoute route)\n            => SetUpRoute(route, SetUpDownstreamRoute(route, fileConfiguration.GlobalConfiguration));\n        return fileConfiguration.Routes\n            .Select(CreateRoute)\n            .ToArray();\n    }\n\n    public virtual int CreateTimeout(FileRoute route, FileGlobalConfiguration global)\n    {\n        int def = DownstreamRoute.DefaultTimeoutSeconds;\n        return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def;\n    }\n\n    private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration)\n    {\n        var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration);\n\n        var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);\n\n        var authOptions = _authOptionsCreator.Create(fileRoute, globalConfiguration);\n\n        var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest);\n\n        var claimsToClaims = _claimsToThingCreator.Create(fileRoute.AddClaimsToRequest);\n\n        var claimsToQueries = _claimsToThingCreator.Create(fileRoute.AddQueriesToRequest);\n\n        var claimsToDownstreamPath = _claimsToThingCreator.Create(fileRoute.ChangeDownstreamPathTemplate);\n\n        var qosOptions = _qosOptionsCreator.Create(fileRoute, globalConfiguration);\n\n        var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute, globalConfiguration);\n\n        var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute, globalConfiguration);\n\n        var hAndRs = _headerFAndRCreator.Create(fileRoute, globalConfiguration);\n\n        var downstreamAddresses = _downstreamAddressesCreator.Create(fileRoute);\n\n        var lbOptions = _loadBalancerOptionsCreator.Create(fileRoute, globalConfiguration);\n        var lbKey = _routeKeyCreator.Create(fileRoute, lbOptions);\n\n        var securityOptions = _securityOptionsCreator.Create(fileRoute.SecurityOptions, globalConfiguration);\n\n        var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion);\n\n        var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy);\n\n        var cacheOptions = _cacheOptionsCreator.Create(fileRoute, globalConfiguration, lbKey);\n\n        var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration);\n\n        var route = new DownstreamRouteBuilder()\n            .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream)\n            .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream)\n            .WithAuthenticationOptions(authOptions)\n            .WithCacheOptions(cacheOptions)\n            .WithClaimsToClaims(claimsToClaims)\n            .WithClaimsToDownstreamPath(claimsToDownstreamPath)\n            .WithClaimsToHeaders(claimsToHeaders)\n            .WithClaimsToQueries(claimsToQueries)\n            .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator)\n            .WithDelegatingHandlers(fileRoute.DelegatingHandlers)\n            .WithDownstreamAddresses(downstreamAddresses)\n            .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream)\n            .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod)\n            .WithDownstreamHttpVersion(downstreamHttpVersion)\n            .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy)\n            .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate)\n            .WithDownstreamScheme(fileRoute.DownstreamScheme)\n            .WithHttpHandlerOptions(httpHandlerOptions)\n            .WithKey(fileRoute.Key)\n            .WithLoadBalancerKey(lbKey)\n            .WithLoadBalancerOptions(lbOptions)\n            .WithMetadata(metadata)\n            .WithQosOptions(qosOptions)\n            .WithRateLimitOptions(rateLimitOption)\n            .WithRequestIdKey(requestIdKey)\n            .WithRouteClaimsRequirement(fileRoute.RouteClaimsRequirement)\n            .WithSecurityOptions(securityOptions)\n            .WithServiceName(fileRoute.ServiceName)\n            .WithServiceNamespace(fileRoute.ServiceNamespace)\n            .WithTimeout(CreateTimeout(fileRoute, globalConfiguration))\n            .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream)\n            .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod.ToList())\n            .WithUpstreamPathTemplate(upstreamTemplatePattern)\n            .Build();\n        return route;\n    }\n\n    private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoute)\n    {\n        var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamPathTemplate\n        var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamHeaders\n        var upstreamHttpMethods = fileRoute.UpstreamHttpMethod.ToHttpMethods();\n        return new Route(downstreamRoute)\n        {\n            UpstreamHeaderTemplates = upstreamHeaderTemplates, // downstreamRoute.UpstreamHeaders\n            UpstreamHost = fileRoute.UpstreamHost,\n            UpstreamHttpMethod = upstreamHttpMethods,\n            UpstreamTemplatePattern = upstreamTemplatePattern,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure;\nusing Ocelot.Values;\n\nnamespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// Default creator of upstream templates based on route headers.\n/// </summary>\n/// <remarks>Ocelot feature: Routing based on request header.</remarks>\npublic partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator\n{\n    [GeneratedRegex(@\"(\\{header:.*?\\})\", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, \"en-US\")]\n    private static partial Regex RegexPlaceholders();\n\n    public IDictionary<string, UpstreamHeaderTemplate> Create(IRouteUpstream route)\n    {\n        return Create(route.UpstreamHeaderTemplates, route.RouteIsCaseSensitive);\n    }\n\n    public IDictionary<string, UpstreamHeaderTemplate> Create(IHeaderDictionary upstreamHeaderTemplates, bool routeIsCaseSensitive)\n    {\n        var headers = upstreamHeaderTemplates.ToDictionary(h => h.Key, h => h.Value.ToString()); // TODO Review usage\n        return Create(headers, routeIsCaseSensitive);\n    }\n\n    protected virtual IDictionary<string, UpstreamHeaderTemplate> Create(IDictionary<string, string> upstreamHeaderTemplates, bool routeIsCaseSensitive)\n    {\n        var result = new Dictionary<string, UpstreamHeaderTemplate>();\n        foreach (var headerTemplate in upstreamHeaderTemplates)\n        {\n            var headerTemplateValue = headerTemplate.Value;\n            var matches = RegexPlaceholders().Matches(headerTemplateValue);\n\n            if (matches.Count > 0)\n            {\n                var placeholders = matches.Select(m => m.Groups[1].Value).ToArray();\n                for (int i = 0; i < placeholders.Length; i++)\n                {\n                    var placeholder = placeholders[i];\n                    var placeholderName = placeholder[8..^1]; // remove \"{header:\" and \"}\"\n                    headerTemplateValue = headerTemplateValue.Replace(placeholder, $\"(?<{placeholderName}>.+)\");\n                }\n            }\n\n            var template = routeIsCaseSensitive\n                ? $\"^{headerTemplateValue}$\"\n                : $\"^(?i){headerTemplateValue}$\"; // ignore case\n\n            result.Add(headerTemplate.Key, new(template, headerTemplate.Value));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs",
    "content": "using Ocelot.Cache;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure;\r\nusing Ocelot.Values;\r\n\r\nnamespace Ocelot.Configuration.Creator;\r\n\r\npublic class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator\r\n{\r\n    public const string RegExMatchZeroOrMoreOfEverything = \".*\";\r\n    private const string RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash = \"[^/]+\";\r\n    private const string RegExMatchEndString = \"$\";\r\n    private const string RegExIgnoreCase = \"(?i)\";\r\n    private const string RegExForwardSlashOnly = \"^/$\";\r\n    private const string RegExForwardSlashAndOnePlaceHolder = \"^/.*\";\r\n    private readonly IOcelotCache<Regex> _cache;\r\n\r\n    public UpstreamTemplatePatternCreator(IOcelotCache<Regex> cache)\r\n    {\r\n        _cache = cache;\r\n    }\r\n\r\n    public UpstreamPathTemplate Create(IRouteUpstream route)\r\n    {\r\n        var upstreamTemplate = route.UpstreamPathTemplate;\r\n        var placeholders = new List<string>();\r\n\r\n        for (var i = 0; i < upstreamTemplate.Length; i++)\r\n        {\r\n            if (IsPlaceHolder(upstreamTemplate, i))\r\n            {\r\n                var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i);\r\n                var difference = postitionOfPlaceHolderClosingBracket - i + 1;\r\n                var placeHolderName = upstreamTemplate.Substring(i, difference);\r\n                placeholders.Add(placeHolderName);\r\n\r\n                // Hack to handle /{url} case\r\n                if (ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket))\r\n                {\r\n                    return CreateTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false, route.UpstreamPathTemplate);\r\n                }\r\n            }\r\n        }\r\n\r\n        var containsQueryString = false;\r\n\r\n        if (upstreamTemplate.Contains('?'))\r\n        {\r\n            containsQueryString = true;\r\n            upstreamTemplate = upstreamTemplate.Replace(\r\n                upstreamTemplate.Contains(\"/?\") ? \"/?\" : \"?\",\r\n                @\"(/$|/\\?|\\?|$)\");\r\n        }\r\n\r\n        for (var i = 0; i < placeholders.Count; i++)\r\n        {\r\n            var indexOfPlaceholder = upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal);\r\n            var indexOfNextForwardSlash = upstreamTemplate.IndexOf(\"/\", indexOfPlaceholder, StringComparison.Ordinal);\r\n            if (indexOfNextForwardSlash < indexOfPlaceholder || (containsQueryString && upstreamTemplate.IndexOf('?', StringComparison.Ordinal) < upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal)))\r\n            {\r\n                upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchZeroOrMoreOfEverything);\r\n            }\r\n            else\r\n            {\r\n                upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash);\r\n            }\r\n        }\r\n\r\n        if (upstreamTemplate == \"/\")\r\n        {\r\n            return CreateTemplate(RegExForwardSlashOnly, route.Priority, containsQueryString, route.UpstreamPathTemplate);\r\n        }\n\n        var index = upstreamTemplate.LastIndexOf('/'); // index of last forward slash\r\n        if (index < (upstreamTemplate.Length - 1) && upstreamTemplate[index + 1] == '.')\r\n        {\r\n            upstreamTemplate = upstreamTemplate[..index] + \"(?:|/\" + upstreamTemplate[++index..] + \")\";\r\n        }\r\n\r\n        if (upstreamTemplate.EndsWith(\"/\"))\r\n        {\r\n            upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length - 1, 1) + \"(/|)\";\r\n        }\r\n\r\n        var template = route.RouteIsCaseSensitive\r\n            ? $\"^{upstreamTemplate}{RegExMatchEndString}\"\r\n            : $\"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}\";\r\n\r\n        return CreateTemplate(template, route.Priority, containsQueryString, route.UpstreamPathTemplate);\r\n    }\r\n\r\n    /// <summary>Time-to-live for caching <see cref=\"Regex\"/> to initialize the <see cref=\"UpstreamPathTemplate.Pattern\"/> property.</summary>\r\n    /// <value>A constant <see cref=\"TimeSpan\"/> structure, default absolute value is 1 minute.</value>\r\n    public static TimeSpan RegexCachingTTL { get; set; } = TimeSpan.FromMinutes(1.0D);\r\n\r\n    protected Regex GetRegex(string key)\r\n    {\r\n        if (string.IsNullOrEmpty(key))\r\n        {\r\n            return null;\r\n        }\r\n\r\n        if (!_cache.TryGetValue(key, nameof(UpstreamPathTemplate), out var rgx))\r\n        {\r\n            rgx = RegexGlobal.New(key, RegexOptions.Singleline);\r\n            _cache.Add(key, rgx, nameof(UpstreamPathTemplate), RegexCachingTTL);\r\n        }\r\n\r\n        return rgx;\r\n    }\r\n\r\n    protected UpstreamPathTemplate CreateTemplate(string template, int priority, bool containsQueryString, string originalValue)\r\n        => new(template, priority, containsQueryString, originalValue)\r\n        {\r\n            Pattern = GetRegex(template),\r\n        };\r\n\r\n    private static bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List<string> placeholders, int postitionOfPlaceHolderClosingBracket)\r\n        => upstreamTemplate.Substring(0, 2) == \"/{\" &&\r\n            placeholders.Count == 1 &&\r\n            upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1;\r\n\r\n    private static bool IsPlaceHolder(string upstreamTemplate, int i)\r\n        => upstreamTemplate[i] == '{';\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Creator/VersionPolicies.cs",
    "content": "﻿namespace Ocelot.Configuration.Creator;\n\n/// <summary>\n/// Constants for conversions in concrete classes for the <see cref=\"IVersionPolicyCreator\"/> interface.\n/// </summary>\npublic class VersionPolicies\n{\n    public const string RequestVersionExact = nameof(RequestVersionExact);\n    public const string RequestVersionOrLower = nameof(RequestVersionOrLower);\n    public const string RequestVersionOrHigher = nameof(RequestVersionOrHigher);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/DownstreamHostAndPort.cs",
    "content": "﻿namespace Ocelot.Configuration;\r\n\r\npublic class DownstreamHostAndPort\r\n{\r\n    public DownstreamHostAndPort(string host, int port)\r\n    {\r\n        Host = host;\r\n        Port = port;\r\n    }\r\n\r\n    public string Host { get; }\r\n    public int Port { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/DownstreamRoute.cs",
    "content": "using Ocelot.Configuration.Creator;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Values;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\npublic class DownstreamRoute\r\n{\r\n    public DownstreamRoute(\r\n        string key,\r\n        UpstreamPathTemplate upstreamPathTemplate,\r\n        List<HeaderFindAndReplace> upstreamHeadersFindAndReplace,\r\n        List<HeaderFindAndReplace> downstreamHeadersFindAndReplace,\r\n        List<DownstreamHostAndPort> downstreamAddresses,\r\n        string serviceName,\r\n        string serviceNamespace,\r\n        HttpHandlerOptions httpHandlerOptions,\r\n        QoSOptions qosOptions,\r\n        string downstreamScheme,\r\n        string requestIdKey,\r\n        CacheOptions cacheOptions,\r\n        LoadBalancerOptions loadBalancerOptions,\r\n        RateLimitOptions rateLimitOptions,\r\n        Dictionary<string, string> routeClaimsRequirement,\r\n        List<ClaimToThing> claimsToQueries,\r\n        List<ClaimToThing> claimsToHeaders,\r\n        List<ClaimToThing> claimsToClaims,\r\n        List<ClaimToThing> claimsToPath,\r\n        AuthenticationOptions authenticationOptions,\r\n        DownstreamPathTemplate downstreamPathTemplate,\r\n        string loadBalancerKey,\r\n        List<string> delegatingHandlers,\r\n        List<AddHeader> addHeadersToDownstream,\r\n        List<AddHeader> addHeadersToUpstream,\r\n        bool dangerousAcceptAnyServerCertificateValidator,\r\n        SecurityOptions securityOptions,\r\n        string downstreamHttpMethod,\r\n        Version downstreamHttpVersion,\n        HttpVersionPolicy downstreamHttpVersionPolicy,\r\n        Dictionary<string, UpstreamHeaderTemplate> upstreamHeaders,\r\n        MetadataOptions metadataOptions,\n        int? timeout)\r\n    {\r\n        DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;\r\n        AddHeadersToDownstream = addHeadersToDownstream;\r\n        DelegatingHandlers = delegatingHandlers;\r\n        Key = key;\r\n        UpstreamPathTemplate = upstreamPathTemplate;\r\n        UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace ?? new List<HeaderFindAndReplace>();\r\n        DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace ?? new List<HeaderFindAndReplace>();\r\n        DownstreamAddresses = downstreamAddresses ?? new List<DownstreamHostAndPort>();\r\n        ServiceName = serviceName;\r\n        ServiceNamespace = serviceNamespace;\r\n        HttpHandlerOptions = httpHandlerOptions;\r\n        QosOptions = qosOptions;\r\n        DownstreamScheme = downstreamScheme;\r\n        RequestIdKey = requestIdKey;\r\n        CacheOptions = cacheOptions;\r\n        LoadBalancerOptions = loadBalancerOptions;\r\n        RateLimitOptions = rateLimitOptions;\r\n        RouteClaimsRequirement = routeClaimsRequirement;\r\n        ClaimsToQueries = claimsToQueries ?? new List<ClaimToThing>();\r\n        ClaimsToHeaders = claimsToHeaders ?? new List<ClaimToThing>();\r\n        ClaimsToClaims = claimsToClaims ?? new List<ClaimToThing>();\r\n        ClaimsToPath = claimsToPath ?? new List<ClaimToThing>();\r\n        AuthenticationOptions = authenticationOptions;\r\n        DownstreamPathTemplate = downstreamPathTemplate;\r\n        LoadBalancerKey = loadBalancerKey;\r\n        AddHeadersToUpstream = addHeadersToUpstream;\r\n        SecurityOptions = securityOptions;\r\n        DownstreamHttpMethod = downstreamHttpMethod;\r\n        DownstreamHttpVersion = downstreamHttpVersion;\n        DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy;\n        UpstreamHeaders = upstreamHeaders ?? new();\r\n        MetadataOptions = metadataOptions;\n        Timeout = timeout;\r\n    }\r\n\r\n    public string Key { get; }\r\n    public UpstreamPathTemplate UpstreamPathTemplate { get; }\r\n    public List<HeaderFindAndReplace> UpstreamHeadersFindAndReplace { get; }\r\n    public List<HeaderFindAndReplace> DownstreamHeadersFindAndReplace { get; }\r\n    public List<DownstreamHostAndPort> DownstreamAddresses { get; }\r\n    public string ServiceName { get; }\r\n    public string ServiceNamespace { get; }\r\n    public HttpHandlerOptions HttpHandlerOptions { get; }\r\n    public QoSOptions QosOptions { get; }\r\n    public string DownstreamScheme { get; }\r\n    public string RequestIdKey { get; }\r\n    public CacheOptions CacheOptions { get; }\r\n    public LoadBalancerOptions LoadBalancerOptions { get; }\r\n    public RateLimitOptions RateLimitOptions { get; }\r\n    public Dictionary<string, string> RouteClaimsRequirement { get; }\r\n    public List<ClaimToThing> ClaimsToQueries { get; }\r\n    public List<ClaimToThing> ClaimsToHeaders { get; }\r\n    public List<ClaimToThing> ClaimsToClaims { get; }\r\n    public List<ClaimToThing> ClaimsToPath { get; }\r\n\r\n    public bool IsAuthenticated => AuthenticationOptions is not null && !AuthenticationOptions.AllowAnonymous && AuthenticationOptions.HasScheme;\r\n    public bool IsAuthorized => RouteClaimsRequirement?.Count > 0;\r\n    public AuthenticationOptions AuthenticationOptions { get; }\r\n    public DownstreamPathTemplate DownstreamPathTemplate { get; }\r\n    public string LoadBalancerKey { get; }\r\n    public List<string> DelegatingHandlers { get; }\r\n    public List<AddHeader> AddHeadersToDownstream { get; }\r\n    public List<AddHeader> AddHeadersToUpstream { get; }\r\n    public bool DangerousAcceptAnyServerCertificateValidator { get; }\r\n    public SecurityOptions SecurityOptions { get; }\r\n    public string DownstreamHttpMethod { get; }\r\n    public Version DownstreamHttpVersion { get; }\n\r\n    /// <summary>The <see cref=\"HttpVersionPolicy\"/> enum specifies behaviors for selecting and negotiating the HTTP version for a request.</summary>\r\n    /// <value>An <see cref=\"HttpVersionPolicy\"/> enum value being mapped from a <see cref=\"VersionPolicies\"/> constant.</value>\r\n    /// <remarks>\r\n    /// Related to the <see cref=\"DownstreamHttpVersion\"/> property.\r\n    /// <list type=\"bullet\">\r\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpversionpolicy\">HttpVersionPolicy Enum</see></item>\r\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.httpversion\">HttpVersion Class</see></item>\r\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy\">HttpRequestMessage.VersionPolicy Property</see></item>\r\n    /// </list>\r\n    /// </remarks>\r\n    public HttpVersionPolicy DownstreamHttpVersionPolicy { get; }\n    public Dictionary<string, UpstreamHeaderTemplate> UpstreamHeaders { get; }\r\n    public MetadataOptions MetadataOptions { get; }\r\n\r\n    /// <summary>The timeout duration for the downstream request in seconds.</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value, in seconds.</value>\n    public int? Timeout { get; }\r\n    public const int LowTimeout = 3;  //  3 seconds\r\n    public const int DefTimeout = 90; // 90 seconds\r\n\r\n    /// <summary>Gets or sets the default timeout in seconds for all routes, applicable at both the route-level and globally.\r\n    /// <para>The setter includes a constraint that ensures the assigned value is greater than or equal to <see cref=\"LowTimeout\"/> (3 seconds).</para></summary>\n    /// <remarks>By default, initialized to <see cref=\"DefTimeout\"/> (90 seconds).</remarks>\n    /// <value>An <see cref=\"int\"/> value in seconds.</value>\n    public static int DefaultTimeoutSeconds { get => defaultTimeoutSeconds; set => defaultTimeoutSeconds = value >= LowTimeout ? value : DefTimeout; }\r\n    private static int defaultTimeoutSeconds = DefTimeout;\r\n\r\n    public string Name() => Name(false);\r\n\r\n    /// <summary>Gets the route name depending on whether the service discovery mode is enabled or disabled.</summary>\r\n    /// <returns>A <see cref=\"string\"/> object with the name.</returns>\r\n    public string Name(bool escapePath)\r\n    {\r\n        var path = !string.IsNullOrEmpty(UpstreamPathTemplate?.OriginalValue)\r\n            ? UpstreamPathTemplate.OriginalValue\r\n            : !string.IsNullOrEmpty(DownstreamPathTemplate.Value) // can't be null because it is created by DownstreamRouteBuilder\r\n                ? DownstreamPathTemplate.ToString()\r\n                : \"?\";\r\n        if (escapePath)\r\n        {\r\n            path = path.Replace(\"{\", \"{{\").Replace(\"}\", \"}}\");\r\n        }\r\n\r\n        return UseServiceDiscovery || !string.IsNullOrEmpty(ServiceName)\r\n            ? string.Join(':', ServiceNamespace, ServiceName, path)\r\n            : path;\r\n    }\r\n\r\n    public override string ToString() => LoadBalancerKey;\r\n    public bool UseServiceDiscovery => !ServiceName.IsEmpty();\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/AggregateRouteConfig.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\r\n\r\npublic class AggregateRouteConfig\r\n{\r\n    public string RouteKey { get; set; }\r\n    public string Parameter { get; set; }\r\n    public string JsonPath { get; set; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileAggregateRoute.cs",
    "content": "using Microsoft.AspNetCore.Http;\r\n\r\nnamespace Ocelot.Configuration.File;\r\n\npublic class FileAggregateRoute : IRouteUpstream, IRouteGroup\r\n{\r\n    public string Aggregator { get; set; }\r\n    public int Priority { get; set; } = 1;\n    public bool RouteIsCaseSensitive { get; set; }\r\n    public HashSet<string> RouteKeys { get; set; }\r\n    public List<AggregateRouteConfig> RouteKeysConfig { get; set; }\r\n    public IDictionary<string, string> UpstreamHeaderTemplates { get; set; }\r\n    public string UpstreamHost { get; set; }\r\n    public HashSet<string> UpstreamHttpMethod { get; set; }\r\n    public string UpstreamPathTemplate { get; set; }\r\n\n    public FileAggregateRoute()\n    {\n        Aggregator = default;\n        Priority = 1;\n        RouteIsCaseSensitive = default;\r\n        RouteKeys = new();\r\n        RouteKeysConfig = new();\r\n        UpstreamHeaderTemplates = new Dictionary<string, string>();\r\n        UpstreamHost = default;\r\n        UpstreamHttpMethod = [ HttpMethods.Get ]; // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :)\r\n        UpstreamPathTemplate = default;\r\n    }\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileAuthenticationOptions.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.File;\n\npublic class FileAuthenticationOptions\n{\n    public FileAuthenticationOptions()\n    { }\n\n    public FileAuthenticationOptions(string authScheme) : this()\n        => AuthenticationProviderKeys = [authScheme];\n\n    public FileAuthenticationOptions(FileAuthenticationOptions options)\n    {\n        ArgumentNullException.ThrowIfNull(options);\n        AllowAnonymous = options.AllowAnonymous;\n        AllowedScopes = options.AllowedScopes is null ? null : new(options.AllowedScopes);\n        AuthenticationProviderKey = options.AuthenticationProviderKey;\n        AuthenticationProviderKeys = new string[options.AuthenticationProviderKeys.Length];\n        Array.Copy(options.AuthenticationProviderKeys, AuthenticationProviderKeys, options.AuthenticationProviderKeys.Length);\n    }\n\n    public List<string> AllowedScopes { get; set; }\n\n    /// <summary>Allows anonymous authentication for route when global authentication options are used.</summary>\n    /// <value><see langword=\"true\"/> if it is allowed; otherwise, <see langword=\"false\"/>.</value>\n    public bool? AllowAnonymous { get; set; }\n\n    [Obsolete(\"Use AuthenticationProviderKeys instead of AuthenticationProviderKey! Note that AuthenticationProviderKey will be removed in version 25.0!\")]\n    public string AuthenticationProviderKey { get; set; }\n\n    public string[] AuthenticationProviderKeys { get; set; }\n\n    /// <summary>Checks whether authentication schemes are specified (not empty, exist).</summary>\n    /// <value><see langword=\"true\"/> if an authentication scheme is defined; otherwise, <see langword=\"false\"/>.</value>\n    public bool HasScheme => AuthenticationProviderKey.IsNotEmpty()\n            || AuthenticationProviderKeys?.Any(StringExtensions.IsNotEmpty) == true;\n    public bool HasScope => AllowedScopes?.Exists(StringExtensions.IsNotEmpty) == true;\n\n    public override string ToString() => new StringBuilder()\n        .Append($\"{nameof(AllowAnonymous)}:{AllowAnonymous ?? false},\")\n        .Append($\"{nameof(AllowedScopes)}:[{AllowedScopes.NotNull().Select(x => $\"'{x}'\").Csv()}],\")\n        .Append($\"{nameof(AuthenticationProviderKey)}:'{AuthenticationProviderKey}',\")\n        .Append($\"{nameof(AuthenticationProviderKeys)}:[{AuthenticationProviderKeys.NotNull().Select(x => $\"'{x}'\").Csv()}]\")\n        .ToString();\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileCacheOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileCacheOptions\n{\n    public FileCacheOptions() { }\n    public FileCacheOptions(int ttl) => TtlSeconds = ttl;\n    public FileCacheOptions(FileCacheOptions from)\n    {\n        Region = from.Region;\n        TtlSeconds = from.TtlSeconds;\n        Header = from.Header;\n        EnableContentHashing = from.EnableContentHashing;\n    }\n\n    /// <summary>Using <see cref=\"Nullable{T}\"/> where T is <see cref=\"int\"/> to have <see langword=\"null\"/> as default value and allowing global configuration usage.</summary>\n    /// <remarks>If <see langword=\"null\"/> then use global configuration with 0 by default.</remarks>\n    /// <value>The time to live seconds, with 0 by default.</value>\n    public int? TtlSeconds { get; set; }\n    public string Region { get; set; }\n    public string Header { get; set; }\n\n    /// <summary>Using <see cref=\"Nullable{T}\"/> where T is <see cref=\"bool\"/> to have <see langword=\"null\"/> as default value and allowing global configuration usage.</summary>\n    /// <remarks>If <see langword=\"null\"/> then use global configuration with <see langword=\"false\"/> by default.</remarks>\n    /// <value><see langword=\"true\"/> if content hashing is enabled; otherwise, <see langword=\"false\"/>.</value>\n    public bool? EnableContentHashing { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileConfiguration.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\r\n\r\npublic class FileConfiguration\r\n{\r\n    public FileConfiguration()\r\n    {\r\n        Routes = new();\r\n        DynamicRoutes = new();\r\n        Aggregates = new();\r\n        GlobalConfiguration = new();\r\n    }\r\n\r\n    public List<FileRoute> Routes { get; set; }\r\n    public List<FileDynamicRoute> DynamicRoutes { get; set; }\r\n\r\n    // Seperate field for aggregates because this let's you re-use Routes in multiple Aggregates\r\n    public List<FileAggregateRoute> Aggregates { get; set; }\r\n\r\n    public FileGlobalConfiguration GlobalConfiguration { get; set; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileDynamicRoute.cs",
    "content": "namespace Ocelot.Configuration.File;\r\n\r\n/// <summary>\r\n/// Represents the JSON structure of a dynamic route in dynamic routing mode using service discovery.\r\n/// </summary>\r\npublic class FileDynamicRoute : FileRouteBase, IRouteGrouping, IRouteRateLimiting\r\n{\r\n    [Obsolete(\"Use RateLimitOptions instead of RateLimitRule! Note that RateLimitRule will be removed in version 25.0!\")]\r\n    public FileRateLimitByHeaderRule RateLimitRule { get; set; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalAuthenticationOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalAuthenticationOptions : FileAuthenticationOptions, IRouteGroup\n{\n    public FileGlobalAuthenticationOptions() : base() { }\n    public FileGlobalAuthenticationOptions(string authScheme) : base(authScheme) { }\n    public FileGlobalAuthenticationOptions(FileAuthenticationOptions from) : base(from) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalCacheOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalCacheOptions : FileCacheOptions, IRouteGroup\n{\n    public FileGlobalCacheOptions() : base() { }\n    public FileGlobalCacheOptions(FileCacheOptions from) : base(from) { }\n    public FileGlobalCacheOptions(int ttl) : base(ttl) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalConfiguration.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalConfiguration\n{\n    public FileGlobalConfiguration()\n    {\n        AuthenticationOptions = new();\n        BaseUrl = default;\n        CacheOptions = default;\n        DownstreamHeaderTransform = new Dictionary<string, string>();\n        DownstreamHttpVersion = default;\n        DownstreamHttpVersionPolicy = default;\n        DownstreamScheme = default;\n        HttpHandlerOptions = new();\n        LoadBalancerOptions = default;\n        Metadata = default;\n        MetadataOptions = new();\n        QoSOptions = default;\n        RateLimitOptions = default;\n        RequestIdKey = default;\n        SecurityOptions = new();\n        ServiceDiscoveryProvider = new();\n        Timeout = null;\n        UpstreamHeaderTransform = new Dictionary<string, string>();\n    }\n\n    public FileGlobalAuthenticationOptions AuthenticationOptions { get; set; }\n    public string BaseUrl { get; set; }\n    public FileGlobalCacheOptions CacheOptions { get; set; }\n    public IDictionary<string, string> DownstreamHeaderTransform { get; set; }\n    public string DownstreamHttpVersion { get; set; }\n    public string DownstreamHttpVersionPolicy { get; set; }\n    public string DownstreamScheme { get; set; }\n    public FileGlobalHttpHandlerOptions HttpHandlerOptions { get; set; }\n    public FileGlobalLoadBalancerOptions LoadBalancerOptions { get; set; }\n    public IDictionary<string, string> Metadata { get; set; }\n    public FileMetadataOptions MetadataOptions { get; set; }\n    public FileGlobalQoSOptions QoSOptions { get; set; }\n    public FileGlobalRateLimitByHeaderRule RateLimitOptions { get; set; }\n    public string RequestIdKey { get; set; }\n    public FileSecurityOptions SecurityOptions { get; set; }\n    public FileServiceDiscoveryProvider ServiceDiscoveryProvider { get; set; }\n\n    /// <summary>Explicit timeout value which overrides default <see cref=\"DownstreamRoute.DefaultTimeoutSeconds\"/>.</summary>\n    /// <remarks>Notes:\n    /// <list type=\"bullet\">\n    ///   <item><see cref=\"DownstreamRoute.Timeout\"/> is the consumer of this property.</item>\n    ///   <item><see cref=\"DownstreamRoute.DefaultTimeoutSeconds\"/> implicitly overrides this property if not defined (null).</item>\n    ///   <item><see cref=\"QoSOptions.Timeout\"/> explicitly overrides this property if QoS is enabled.</item>\n    /// </list>\n    /// </remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value, in seconds.</value>\n    public int? Timeout { get; set; }\n    public IDictionary<string, string> UpstreamHeaderTransform { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalHttpHandlerOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalHttpHandlerOptions : FileHttpHandlerOptions, IRouteGroup\n{\n    public FileGlobalHttpHandlerOptions() : base() { }\n    public FileGlobalHttpHandlerOptions(FileHttpHandlerOptions from) : base(from) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalLoadBalancerOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalLoadBalancerOptions : FileLoadBalancerOptions, IRouteGroup\n{\n    public FileGlobalLoadBalancerOptions() : base() { }\n    public FileGlobalLoadBalancerOptions(string type) : base(type) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalQoSOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalQoSOptions : FileQoSOptions, IRouteGroup\n{\n    public FileGlobalQoSOptions() : base() { }\n    public FileGlobalQoSOptions(FileQoSOptions from) : base(from) { }\n    public FileGlobalQoSOptions(QoSOptions from) : base(from) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimit.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic sealed class FileGlobalRateLimit :\n    FileGlobalRateLimitByMethodRule, // TODO This is temporarily solution to inherit from RL by Header feature model, an extraction of props is required\n    IRouteGroup\n{\n    // TODO Potentially, it should be 'Policy Name', or something that conveys the meaning of 'Rule Name'\n    public string Name { get; init; }\n\n    public string Pattern { get; init; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimitByAspNetRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalRateLimitByAspNetRule : FileRateLimitByAspNetRule, IRouteGroup\n{\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalRateLimitByHeaderRule : FileRateLimitByHeaderRule, IRouteGroup\n{\n    public FileGlobalRateLimitByHeaderRule()\n        : base() { }\n    public FileGlobalRateLimitByHeaderRule(FileRateLimitByHeaderRule from)\n        : base(from) { }\n\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimitByIpRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalRateLimitByIpRule : FileRateLimitByIpRule, IRouteGroup\n{\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimitByMethodRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalRateLimitByMethodRule : FileRateLimitByMethodRule, IRouteGroup\n{\n    /// <summary>Gets or sets the keys used to group routes, based on the already defined <see cref=\"FileRouteBase.Key\"/> property.</summary>\n    /// <remarks>If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes.</remarks>\n    /// <value>A <see cref=\"HashSet{T}\"/> (where <c>T</c> is <see cref=\"string\"/>) collection of keys that determine which routes the options should be applied to.</value>\n    public HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileGlobalRateLimiting.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileGlobalRateLimiting : FileRateLimitRule\n{\n    public FileGlobalRateLimitByHeaderRule[] ByHeader { get; set; }\n    public /*FileGlobalRateLimitByMethodRule[]*/ FileGlobalRateLimit[] ByMethod { get; set; } // a prototype solution must be designed. Methods -> GET, POST, PUT etc.\n    public FileGlobalRateLimitByIpRule[] ByIP { get; set; } // a prototype solution must be designed. Based on RemoteIpAddress\n    public FileGlobalRateLimitByAspNetRule[] ByAspNet { get; set; } // a prototype solution must be designed\n    public IDictionary<string, string> Metadata { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileHostAndPort.cs",
    "content": "namespace Ocelot.Configuration.File;\r\n\r\npublic class FileHostAndPort\r\n{\n    public FileHostAndPort() { }\n\n    public FileHostAndPort(FileHostAndPort from)\n    {\n        Host = from.Host;\n        Port = from.Port;\n    }\n\n    public FileHostAndPort(string host, int port)\n    {\n        Host = host;\n        Port = port;\n    }\n\r\n    public string Host { get; set; }\r\n    public int Port { get; set; }\r\n\r\n    public override string ToString() => $\"{Host}:{Port}\";\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\r\n\r\npublic class FileHttpHandlerOptions\r\n{\r\n    public FileHttpHandlerOptions()\r\n    { }\r\n\n    public FileHttpHandlerOptions(FileHttpHandlerOptions from)\r\n    {\r\n        AllowAutoRedirect = from.AllowAutoRedirect;\r\n        MaxConnectionsPerServer = from.MaxConnectionsPerServer;\n        PooledConnectionLifetimeSeconds = from.PooledConnectionLifetimeSeconds;\n        UseCookieContainer = from.UseCookieContainer;\n        UseProxy = from.UseProxy;\r\n        UseTracing = from.UseTracing;\n    }\r\n\r\n    public bool? AllowAutoRedirect { get; set; }\r\n    public int? MaxConnectionsPerServer { get; set; }\r\n    public int? PooledConnectionLifetimeSeconds { get; set; }\r\n    public bool? UseCookieContainer { get; set; }\r\n    public bool? UseProxy { get; set; }\n    public bool? UseTracing { get; set; }\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs",
    "content": "namespace Ocelot.Configuration.File;\n\npublic class FileLoadBalancerOptions\n{\n    public FileLoadBalancerOptions()\n    { }\n\n    public FileLoadBalancerOptions(string type)\n        : this()\n    {\n        Type = type;\n    }\n\n    public FileLoadBalancerOptions(FileLoadBalancerOptions from)\n    {\n        Expiry = from.Expiry;\n        Key = from.Key;\n        Type = from.Type;\n    }\n\n    public int? Expiry { get; set; }\n    public string Key { get; set; }\n    public string Type { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileMetadataOptions.cs",
    "content": "﻿using System.Globalization;\n\nnamespace Ocelot.Configuration.File;\n\npublic class FileMetadataOptions\n{\n    public FileMetadataOptions()\n    {\n        CurrentCulture = CultureInfo.CurrentCulture.Name;\n        NumberStyle = Enum.GetName(NumberStyles.Any);\n        Separators = new[] { \",\" };\n        StringSplitOption = Enum.GetName(StringSplitOptions.None);\n        TrimChars = new[] { ' ' };\n    }\n\n    public FileMetadataOptions(FileMetadataOptions from)\n    {\n        CurrentCulture = from.CurrentCulture;\n        NumberStyle = from.NumberStyle;\n        Separators = from.Separators;\n        StringSplitOption = from.StringSplitOption;\n        TrimChars = from.TrimChars;\n    }\n\n    public string CurrentCulture { get; set; }\n    public string NumberStyle { get; set; }\n    public string[] Separators { get; set; }\n    public string StringSplitOption { get; set; }\n    public char[] TrimChars { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileQoSOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\n/// <summary>\n/// File model for the \"Quality of Service\" feature options of the route.\n/// </summary>\npublic class FileQoSOptions\n{\n    /// <summary>Initializes a new instance of the <see cref=\"FileQoSOptions\"/> class.</summary>\n    public FileQoSOptions()\n    { }\n\n    public FileQoSOptions(FileQoSOptions from)\n    {\n        DurationOfBreak = from.DurationOfBreak;\n        BreakDuration = from.BreakDuration;\n        ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking;\n        MinimumThroughput = from.MinimumThroughput;\n        FailureRatio = from.FailureRatio;\n        SamplingDuration = from.SamplingDuration;\n        TimeoutValue = from.TimeoutValue;\n        Timeout = from.Timeout;\n    }\n\n    public FileQoSOptions(QoSOptions from)\n    {\n        DurationOfBreak = from.BreakDuration;\n        BreakDuration = from.BreakDuration;\n        ExceptionsAllowedBeforeBreaking = from.MinimumThroughput;\n        MinimumThroughput = from.MinimumThroughput;\n        FailureRatio = from.FailureRatio;\n        SamplingDuration = from.SamplingDuration;\n        TimeoutValue = from.Timeout;\n        Timeout = from.Timeout;\n    }\n\n    [Obsolete(\"Use BreakDuration instead of DurationOfBreak! Note that DurationOfBreak will be removed in version 25.0!\")]\n    public int? DurationOfBreak { get; set; }\n    public int? BreakDuration { get; set; }\n\n    [Obsolete(\"Use MinimumThroughput instead of ExceptionsAllowedBeforeBreaking! Note that ExceptionsAllowedBeforeBreaking will be removed in version 25.0!\")]\n    public int? ExceptionsAllowedBeforeBreaking { get; set; }\n    public int? MinimumThroughput { get; set; }\n\n    public double? FailureRatio { get; set; }\n    public int? SamplingDuration { get; set; }\n\n    /// <summary>Explicit timeout value which overrides default one.</summary>\n    /// <remarks>Reused in, or ignored in favor of implicit default value:\n    /// <list type=\"bullet\">\n    ///   <item><see cref=\"QoSOptions.Timeout\"/></item>\n    ///   <item><see cref=\"DownstreamRoute.Timeout\"/></item>\n    ///   <item><see cref=\"DownstreamRoute.DefaultTimeoutSeconds\"/></item>\n    /// </list>\n    /// </remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value in milliseconds.</value>\n    [Obsolete(\"Use Timeout instead of TimeoutValue! Note that TimeoutValue will be removed in version 25.0!\")]\n    public int? TimeoutValue { get; set; }\n    public int? Timeout { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimitByAspNetRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileRateLimitByAspNetRule\n{\n    /// <summary>Gets or sets the policy name of ASP.NET Core rate limiter.</summary>\n    /// <value>A <see cref=\"string\"/> representing the policy name.</value>\n    public string Policy { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimitByHeaderRule.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.RateLimiting;\n\nnamespace Ocelot.Configuration.File;\n\npublic class FileRateLimitByHeaderRule : FileRateLimitRule\n{\n    public FileRateLimitByHeaderRule() : base()\n    { }\n\n    public FileRateLimitByHeaderRule(FileRateLimitRule from)\n        : base(from)\n    { }\n\n    public FileRateLimitByHeaderRule(FileRateLimitByHeaderRule from)\n        : base(from)\n    {\n        ClientIdHeader = from.ClientIdHeader;\n        ClientWhitelist = from.ClientWhitelist;\n        DisableRateLimitHeaders = from.DisableRateLimitHeaders;\n        HttpStatusCode = from.HttpStatusCode;\n        QuotaExceededMessage = from.QuotaExceededMessage;\n        RateLimitCounterPrefix = from.RateLimitCounterPrefix;\n    }\n\n    /// <summary>Gets or sets the HTTP header used to store the client identifier, which defaults to <c>Oc-Client</c>.</summary>\n    /// <value>A <see cref=\"string\"/> representing the name of the HTTP header.</value>\n    public string ClientIdHeader { get; set; }\n\n    /// <summary>A list of approved clients aka whitelisted ones.</summary>\n    /// <value>An <see cref=\"IList{T}\"/> collection of allowed clients.</value>\n    public IList<string> ClientWhitelist { get; set; }\n\n    /// <summary>\n    /// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled (<see cref=\"FileRateLimitRule.EnableRateLimiting\"/> is <see langword=\"false\"/>).\n    /// </summary>\n    /// <remarks>Format: <c>H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]</c>.</remarks>\n    /// <returns>A <see cref=\"string\"/> object.</returns>\n    public override string ToString()\n    {\n        if (EnableRateLimiting == false)\n        {\n            return string.Empty;\n        }\n\n        /*\n        string baseString = base.ToString();\n        char hdrSign = DisableRateLimitHeaders.HasValue\n            ? (DisableRateLimitHeaders.Value ? '-' : '+')\n            : baseString[1];\n        if (DisableRateLimitHeaders.HasValue && baseString[1] != hdrSign)\n        {\n            Span<byte> span = stackalloc byte[Encoding.ASCII.GetByteCount(baseString)];\n            Encoding.ASCII.GetBytes(baseString, span);\n            span[1] = (byte)hdrSign; // replace hdr sign\n            baseString = Encoding.ASCII.GetString(span);\n        }*/\n        var baseString = base.ToString();\n        if (DisableRateLimitHeaders is bool disabled && baseString[1] != (disabled ? '-' : '+'))\n        {\n            baseString = string.Create(baseString.Length, (baseString, disabled), static (dstSpan, state) =>\n            {\n                state.baseString.AsSpan().CopyTo(dstSpan);\n                dstSpan[1] = state.disabled ? '-' : '+';\n            });\n        }\n\n        string clHdr = ClientIdHeader.IfEmpty(None);\n        string clLst = ClientWhitelist is null ? None : '[' + string.Join(',', ClientWhitelist) + ']';\n        return $\"{baseString}/HDR:{clHdr}/WL{clLst}\";\n    }\n\n    /// <summary>Disables or enables <c>X-RateLimit-*</c> and <c>Retry-After</c> headers.</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see cref=\"bool\"/>.</value>\n    [Obsolete(\"Use EnableHeaders instead of DisableRateLimitHeaders! Note that DisableRateLimitHeaders will be removed in version 25.0!\")]\n    public bool? DisableRateLimitHeaders { get; set; }\n\n    /// <summary>Gets or sets the rejection status code returned during the Quota Exceeded period, aka the <see cref=\"FileRateLimitRule.PeriodTimespan\"/> wait window, or the remainder of the <see cref=\"FileRateLimitRule.Period\"/> fixed window following the moment of exceeding.\n    /// <para>Default value: <see href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429\">429 (Too Many Requests)</see>.</para></summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see cref=\"int\"/>.</value>\n    [Obsolete(\"Use StatusCode instead of HttpStatusCode! Note that HttpStatusCode will be removed in version 25.0!\")]\n    public int? HttpStatusCode { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message.\n    /// <para>If none specified the default will be: <see cref=\"RateLimitOptions.DefaultQuotaMessage\"/>.</para>\n    /// </summary>\n    /// <value>A <see cref=\"string\"/> value that will be used as a formatter.</value>\n    [Obsolete(\"Use QuotaMessage instead of QuotaExceededMessage! Note that QuotaExceededMessage will be removed in version 25.0!\")]\n    public string QuotaExceededMessage { get; set; }\n\n    /// <summary>Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the <see cref=\"IRateLimitStorage\"/> service.</summary>\n    /// <remarks>Notes:\n    /// <list type=\"number\">\n    /// <item>The consumer is the <see cref=\"IRateLimiting.GetStorageKey(ClientRequestIdentity, RateLimitOptions)\"/> method.</item>\n    /// <item>The property is relevant for distributed storage systems, such as <see cref=\"IDistributedCache\"/> services, to inform users about which objects are being cached for management purposes.\n    /// By default, each Ocelot instance uses its own <see cref=\"IMemoryCache\"/> service without cross-instance synchronization.</item>\n    /// </list>\n    /// </remarks>\n    /// <value>A <see cref=\"string\"/> object which value defaults to \"Ocelot.RateLimiting\", see the <see cref=\"RateLimitOptions.DefaultCounterPrefix\"/> property.</value>\n    [Obsolete(\"Use KeyPrefix instead of RateLimitCounterPrefix! Note that RateLimitCounterPrefix will be removed in version 25.0!\")]\n    public string RateLimitCounterPrefix { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimitByIpRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileRateLimitByIpRule : FileRateLimitRule\n{\n    /// <summary>A list of allowed client's IP addresses aka whitelisted ones.</summary>\n    /// <value>An <see cref=\"IList{T}\"/> collection of allowed IPs.</value>\n    public IList<string> IPWhitelist { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimitByMethodRule.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileRateLimitByMethodRule : FileRateLimitRule\n{\n    public HashSet<string> Methods { get; init; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimitRule.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.RateLimiting;\n\nnamespace Ocelot.Configuration.File;\n\npublic class FileRateLimitRule\n{\n    public FileRateLimitRule() { }\n\n    public FileRateLimitRule(FileRateLimitRule from)\n    {\n        ArgumentNullException.ThrowIfNull(from);\n\n        EnableRateLimiting = from.EnableRateLimiting;\n        EnableHeaders = from.EnableHeaders;\n        Limit = from.Limit;\n        Period = from.Period;\n        PeriodTimespan = from.PeriodTimespan;\n        Wait = from.Wait;\n        StatusCode = from.StatusCode;\n        QuotaMessage = from.QuotaMessage;\n        KeyPrefix = from.KeyPrefix;\n    }\n\n    /// <summary>Enables or disables rate limiting. If undefined, it implicitly defaults to <see langword=\"true\"/> (enabled).</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see langword=\"bool\"/>.</value>\n    public bool? EnableRateLimiting { get; set; }\n\n    /// <summary>Enables or disables <c>X-RateLimit-*</c> and <c>Retry-After</c> headers.</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see langword=\"bool\"/>.</value>\n    public bool? EnableHeaders { get; set; }\n\n    /// <summary>The maximum number of requests a client can make within a given time <see cref=\"Period\"/>.</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see cref=\"long\"/>.</value>\n    public long? Limit { get; set; }\n\n    /// <summary>Rate limiting period (fixed window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d).</summary>\n    /// <remarks>Defaults: If no unit is specified, the default unit is 'ms'.</remarks>\n    /// <value>A <see cref=\"string\"/> object.</value>\n    public string Period { get; set; }\n\n    /// <summary>The time interval to wait before sending a new request, measured in seconds.</summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see cref=\"double\"/>.</value>\n    [Obsolete(\"Use Wait instead of PeriodTimespan! Note that PeriodTimespan will be removed in version 25.0!\")]\n    public double? PeriodTimespan { get; set; }\n\n    /// <summary>Rate limiting wait window (no servicing window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d).</summary>\n    /// <remarks>Defaults: If no unit is specified, the default unit is 'ms'.</remarks>\n    /// <value>A <see cref=\"string\"/> object.</value>\n    public string Wait { get; set; }\n\n    /// <summary>Gets or sets the rejection status code returned during the Quota Exceeded period, aka the <see cref=\"Wait\"/> window, or the remainder of the <see cref=\"Period\"/> fixed window following the moment of exceeding.\n    /// <para>Default value: <see href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429\">429 (Too Many Requests)</see>.</para></summary>\n    /// <value>A <see cref=\"Nullable{T}\"/> value, where <c>T</c> is <see cref=\"int\"/>.</value>\n    public int? StatusCode { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message.\n    /// <para>If none specified the default will be: <see cref=\"RateLimitOptions.DefaultQuotaMessage\"/>.</para>\n    /// </summary>\n    /// <value>A <see cref=\"string\"/> value that will be used as a formatter.</value>\n    public string QuotaMessage { get; set; }\n\n    /// <summary>Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the <see cref=\"IRateLimitStorage\"/> service.</summary>\n    /// <remarks>Notes:\n    /// <list type=\"number\">\n    /// <item>The consumer is the <see cref=\"IRateLimiting.GetStorageKey(ClientRequestIdentity, RateLimitOptions)\"/> method.</item>\n    /// <item>The property is relevant for distributed storage systems, such as <see cref=\"IDistributedCache\"/> services, to inform users about which objects are being cached for management purposes.\n    /// By default, each Ocelot instance uses its own <see cref=\"IMemoryCache\"/> service without cross-instance synchronization.</item>\n    /// </list>\n    /// </remarks>\n    /// <value>A <see cref=\"string\"/> object which value defaults to \"Ocelot.RateLimiting\", see the <see cref=\"RateLimitOptions.DefaultCounterPrefix\"/> property.</value>\n    public string KeyPrefix { get; set; }\n\n    /// <summary>\n    /// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled (<see cref=\"EnableRateLimiting\"/> is <see langword=\"false\"/>).\n    /// </summary>\n    /// <remarks>Format: <c>H{+,-}:{limit}:{period,-}:w{wait,-}</c>.</remarks>\n    /// <returns>A <see cref=\"string\"/> object.</returns>\n    public override string ToString()\n    {\n        if (EnableRateLimiting == false)\n        {\n            return string.Empty;\n        }\n\n        char hdrSign = EnableHeaders == false ? '-' : '+';\n        string waitWindow = PeriodTimespan.HasValue ? PeriodTimespan.Value.ToString(\"F3\") + 's' : Wait.IfEmpty(None);\n        return $\"H{hdrSign}:{Limit}:{Period}:w{waitWindow}\";\n    }\n\n    public const string None = \"-\";\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRateLimiting.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic class FileRateLimiting : FileRateLimitRule\n{\n    public FileRateLimitByHeaderRule ByHeader { get; set; }\n    public /*FileRateLimitByMethodRule*/ FileGlobalRateLimit ByMethod { get; set; } // a prototype solution must be designed. Methods -> GET, POST, PUT etc.\n    public FileRateLimitByIpRule ByIP { get; set; } // a prototype solution must be designed. Based on RemoteIpAddress\n    public FileRateLimitByAspNetRule ByAspNet { get; set; } // a prototype solution must be designed\n    public IDictionary<string, string> Metadata { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRoute.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\r\n\r\n/// <summary>\r\n/// Represents the JSON structure of a standard static route (no service discovery).\r\n/// </summary>\npublic class FileRoute : FileRouteBase, IRouteUpstream, IRouteGrouping, IRouteRateLimiting, ICloneable\r\n{\r\n    public FileRoute()\r\n    {\r\n        AddClaimsToRequest = new Dictionary<string, string>();\r\n        AddHeadersToRequest = new Dictionary<string, string>();\r\n        AddQueriesToRequest = new Dictionary<string, string>();\r\n        ChangeDownstreamPathTemplate = new Dictionary<string, string>();\r\n        DelegatingHandlers = new List<string>();\r\n        DownstreamHeaderTransform = new Dictionary<string, string>();\r\n        DownstreamHostAndPorts = new List<FileHostAndPort>();\r\n        Priority = 1; // to be reviewed WTF?\r\n        RouteClaimsRequirement = new Dictionary<string, string>();\r\n        SecurityOptions = new FileSecurityOptions();\r\n        UpstreamHeaderTemplates = new Dictionary<string, string>();\n        UpstreamHeaderTransform = new Dictionary<string, string>();\r\n        UpstreamHttpMethod = new();\r\n    }\n\n    public FileRoute(FileRoute from)\n    {\n        DeepCopy(from, this);\n    }\n\n    public Dictionary<string, string> AddClaimsToRequest { get; set; }\n    public Dictionary<string, string> AddHeadersToRequest { get; set; }\n    public Dictionary<string, string> AddQueriesToRequest { get; set; }\n    public Dictionary<string, string> ChangeDownstreamPathTemplate { get; set; }\n    public bool DangerousAcceptAnyServerCertificateValidator { get; set; }\n    public List<string> DelegatingHandlers { get; set; }\n    public IDictionary<string, string> DownstreamHeaderTransform { get; set; }\n    public List<FileHostAndPort> DownstreamHostAndPorts { get; set; }\n    public string DownstreamHttpMethod { get; set; }\r\n    public string DownstreamPathTemplate { get; set; }\n    [Obsolete(\"Use CacheOptions instead of FileCacheOptions! Note that FileCacheOptions will be removed in version 25.0!\")]\r\n    public FileCacheOptions FileCacheOptions { get; set; }\n    public int Priority { get; set; }\n    public string RequestIdKey { get; set; }\n    public Dictionary<string, string> RouteClaimsRequirement { get; set; }\n    public bool RouteIsCaseSensitive { get; set; }\n    public FileSecurityOptions SecurityOptions { get; set; }\n    public IDictionary<string, string> UpstreamHeaderTemplates { get; set; }\n    public IDictionary<string, string> UpstreamHeaderTransform { get; set; }\n    public string UpstreamHost { get; set; }\r\n    public HashSet<string> UpstreamHttpMethod { get; set; }\n    public string UpstreamPathTemplate { get; set; }\n\n    /// <summary>\n    /// Clones this object by making a deep copy.\n    /// </summary>\n    /// <returns>A <see cref=\"FileRoute\"/> deeply copied object.</returns>\n    public object Clone()\n    {\n        var other = (FileRoute)MemberwiseClone();\n        DeepCopy(this, other);\n        return other;\n    }\n\n    public static void DeepCopy(FileRoute from, FileRoute to)\n    {\n        to.AddClaimsToRequest = new(from.AddClaimsToRequest);\n        to.AddHeadersToRequest = new(from.AddHeadersToRequest);\n        to.AddQueriesToRequest = new(from.AddQueriesToRequest);\n        to.AuthenticationOptions = from.AuthenticationOptions is null ? null : new(from.AuthenticationOptions);\n        to.ChangeDownstreamPathTemplate = new(from.ChangeDownstreamPathTemplate);\n        to.DangerousAcceptAnyServerCertificateValidator = from.DangerousAcceptAnyServerCertificateValidator;\n        to.DelegatingHandlers = new(from.DelegatingHandlers);\n        to.DownstreamHeaderTransform = new Dictionary<string, string>(from.DownstreamHeaderTransform);\n        to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList();\n        to.DownstreamHttpMethod = from.DownstreamHttpMethod;\n        to.DownstreamHttpVersion = from.DownstreamHttpVersion;\n        to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy;\r\n        to.DownstreamPathTemplate = from.DownstreamPathTemplate;\n        to.DownstreamScheme = from.DownstreamScheme;\r\n        to.CacheOptions = new(from.CacheOptions);\n        to.FileCacheOptions = new(from.FileCacheOptions);\n        to.HttpHandlerOptions = new(from.HttpHandlerOptions);\n        to.Key = from.Key;\n        to.LoadBalancerOptions = new(from.LoadBalancerOptions);\n        to.Metadata = new Dictionary<string, string>(from.Metadata);\r\n        to.Priority = from.Priority;\n        to.QoSOptions = new(from.QoSOptions);\r\n        to.RateLimiting = from.RateLimiting; // new(from.RateLimiting)\n        to.RateLimitOptions = new(from.RateLimitOptions);\n        to.RequestIdKey = from.RequestIdKey;\n        to.RouteClaimsRequirement = new(from.RouteClaimsRequirement);\n        to.RouteIsCaseSensitive = from.RouteIsCaseSensitive;\n        to.SecurityOptions = new(from.SecurityOptions);\n        to.ServiceName = from.ServiceName;\n        to.ServiceNamespace = from.ServiceNamespace;\n        to.Timeout = from.Timeout;\n        to.UpstreamHeaderTemplates = new Dictionary<string, string>(from.UpstreamHeaderTemplates);\n        to.UpstreamHeaderTransform = new Dictionary<string, string>(from.UpstreamHeaderTransform);\n        to.UpstreamHost = from.UpstreamHost;\n        to.UpstreamHttpMethod = new(from.UpstreamHttpMethod);\n        to.UpstreamPathTemplate = from.UpstreamPathTemplate;\n    }\r\n\r\n    public override string ToString()\r\n    {\r\n        if (!string.IsNullOrWhiteSpace(Key))\r\n        {\r\n            return Key;\r\n        }\r\n\r\n        var path = !string.IsNullOrEmpty(UpstreamPathTemplate) ? UpstreamPathTemplate\r\n            : !string.IsNullOrEmpty(DownstreamPathTemplate) ? DownstreamPathTemplate\r\n            : \"?\";\r\n        return !string.IsNullOrWhiteSpace(ServiceName)\r\n            ? string.Join(':', ServiceNamespace, ServiceName, path)\r\n            : path;\r\n    }\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileRouteBase.cs",
    "content": "﻿using Ocelot.Configuration.Creator;\nusing System.Text.Json.Serialization;\nusing NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute;\n\nnamespace Ocelot.Configuration.File;\n\n/// <summary>\n/// Defines common aggregation for dynamic and static routes.\n/// </summary>\npublic abstract class FileRouteBase : IRouteGrouping\n{\n    public FileAuthenticationOptions AuthenticationOptions { get; set; }\n    public FileCacheOptions CacheOptions { get; set; }\n\n    /// <summary>The <see cref=\"HttpVersionPolicy\"/> enum specifies behaviors for selecting and negotiating the HTTP version for a request.</summary>\n    /// <value>A <see langword=\"string\" /> value of defined <see cref=\"VersionPolicies\"/> constants.</value>\n    /// <remarks>\n    /// Related to the <see cref=\"DownstreamHttpVersion\"/> property.\n    /// <list type=\"bullet\">\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpversionpolicy\">HttpVersionPolicy Enum</see></item>\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.httpversion\">HttpVersion Class</see></item>\n    ///   <item><see href=\"https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy\">HttpRequestMessage.VersionPolicy Property</see></item>\n    /// </list>\n    /// </remarks>\n    public string DownstreamHttpVersionPolicy { get; set; }\n    public string DownstreamHttpVersion { get; set; }\n    public string DownstreamScheme { get; set; }\n    public FileHttpHandlerOptions HttpHandlerOptions { get; set; }\n    public string Key { get; set; } // IRouteGrouping\n    public FileLoadBalancerOptions LoadBalancerOptions { get; set; }\n    public IDictionary<string, string> Metadata { get; set; }\n    public FileQoSOptions QoSOptions { get; set; }\n    public FileRateLimitByHeaderRule RateLimitOptions { get; set; } // IRouteRateLimiting\n    [NewtonsoftJsonIgnore, JsonIgnore] // publish the schema in version 25.1!\n    public FileRateLimiting RateLimiting { get; set; }\n    public string ServiceName { get; set; }\n    public string ServiceNamespace { get; set; }\n\n    /// <summary>Explicit timeout value which overrides default <see cref=\"DownstreamRoute.DefaultTimeoutSeconds\"/>.</summary>\n    /// <remarks>Notes:\n    /// <list type=\"bullet\">\n    ///   <item><see cref=\"DownstreamRoute.Timeout\"/> is the consumer of this property.</item>\n    ///   <item><see cref=\"DownstreamRoute.DefaultTimeoutSeconds\"/> implicitly overrides this property if not defined (null).</item>\n    ///   <item><see cref=\"QoSOptions.Timeout\"/> explicitly overrides this property if QoS is enabled.</item>\n    /// </list>\n    /// </remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value, in seconds.</value>\n    public int? Timeout { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileSecurityOptions.cs",
    "content": "namespace Ocelot.Configuration.File;\n\npublic class FileSecurityOptions\n{\n    public FileSecurityOptions()\n    {\n        IPAllowedList = new();\n        IPBlockedList = new();\n        ExcludeAllowedFromBlocked = false;\n    }\n\n    public FileSecurityOptions(FileSecurityOptions from)\n    {\n        IPAllowedList = new(from.IPAllowedList);\n        IPBlockedList = new(from.IPBlockedList);\n        ExcludeAllowedFromBlocked = from.ExcludeAllowedFromBlocked;\n    }\n\n    public FileSecurityOptions(string allowedIPs = null, string blockedIPs = null, bool? excludeAllowedFromBlocked = null)\n        : this()\n    {\n        if (!string.IsNullOrEmpty(allowedIPs))\n        {\n            IPAllowedList.Add(allowedIPs);\n        }\n\n        if (!string.IsNullOrEmpty(blockedIPs))\n        {\n            IPBlockedList.Add(blockedIPs);\n        }\n\n        ExcludeAllowedFromBlocked = excludeAllowedFromBlocked ?? false;\n    }\n\n    public FileSecurityOptions(IEnumerable<string> allowedIPs = null, IEnumerable<string> blockedIPs = null, bool? excludeAllowedFromBlocked = null)\n        : this()\n    {\n        IPAllowedList.AddRange(allowedIPs ?? Enumerable.Empty<string>());\n        IPBlockedList.AddRange(blockedIPs ?? Enumerable.Empty<string>());\n        ExcludeAllowedFromBlocked = excludeAllowedFromBlocked ?? false;\n    }\n\n    public List<string> IPAllowedList { get; set; }\n    public List<string> IPBlockedList { get; set; }\n\n    /// <summary>Provides the ability to specify a wide range of blocked IP addresses and allow a subrange of IP addresses.</summary>\n    /// <value>A <see cref=\"bool\"/> value, defaults to <see langword=\"false\"/>.</value>        \n    public bool ExcludeAllowedFromBlocked { get; set; }\n\n    public bool IsEmpty() => IPAllowedList.Count == 0 && IPBlockedList.Count == 0;\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs",
    "content": "namespace Ocelot.Configuration.File;\n\npublic class FileServiceDiscoveryProvider\n{\n    public string Scheme { get; set; }\n    public string Host { get; set; }\n    public int Port { get; set; }\n    public string Type { get; set; }\n    public string Token { get; set; }\n    public string ConfigurationKey { get; set; }\n    public int PollingInterval { get; set; }\n    public string Namespace { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/IRouteGroup.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\n/// <summary>\n/// Provides support for creating a group of routes, instances of <see cref=\"IRouteGrouping\"/>.\n/// </summary>\npublic interface IRouteGroup\n{\n    /// <summary>The group's list of route keys (the <see cref=\"IRouteGrouping.Key\"/> property).</summary>\n    /// <value>A <see cref=\"HashSet{T}\"/> collection, where <c>T</c> is a <see cref=\"string\"/>, containing key strings.</value>\n    HashSet<string> RouteKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/IRouteGrouping.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\n/// <summary>\n/// Allows to add this route to a group of routes as an <see cref=\"IRouteGroup\"/> object.\n/// </summary>\npublic interface IRouteGrouping\n{\n    /// <summary>The key for this route is used to group it as part of the <see cref=\"IRouteGroup.RouteKeys\"/> collection.</summary>\n    /// <value>A <see cref=\"string\"/> object, containing key.</value>\n    string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/IRouteRateLimiting.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic interface IRouteRateLimiting : IRouteGrouping\n{\n    FileRateLimitByHeaderRule RateLimitOptions { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/File/IRouteUpstream.cs",
    "content": "﻿namespace Ocelot.Configuration.File;\n\npublic interface IRouteUpstream\n{\n    IDictionary<string, string> UpstreamHeaderTemplates { get; }\n    string UpstreamPathTemplate { get; }\n    HashSet<string> UpstreamHttpMethod { get; }\n    bool RouteIsCaseSensitive { get; }\n    int Priority { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/HeaderFindAndReplace.cs",
    "content": "﻿namespace Ocelot.Configuration;\r\n\r\npublic class HeaderFindAndReplace\r\n{\r\n    public const char Comma = ',';\r\n\r\n    public HeaderFindAndReplace(HeaderFindAndReplace from)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(from, nameof(from));\r\n        Index = from.Index;\r\n        Key = from.Key;\r\n        Find = from.Find;\r\n        Replace = from.Replace;\r\n    }\r\n\r\n    public HeaderFindAndReplace(KeyValuePair<string, string> from)\r\n    {\r\n        Index = 0;\r\n        Key = from.Key;\r\n        if (!string.IsNullOrWhiteSpace(from.Value) && from.Value.Contains(Comma))\r\n        {\r\n            string[] parsed = from.Value.Split(Comma);\r\n            Find = parsed[0].Trim();\r\n            Replace = parsed[1].Trim();\r\n        }\r\n        else\r\n        {\r\n            Find = from.Value?.Trim() ?? string.Empty;\r\n            Replace = string.Empty;\r\n        }\r\n    }\r\n\r\n    public HeaderFindAndReplace(string key, string find, string replace, int index)\r\n    {\r\n        Key = key;\r\n        Find = find;\r\n        Replace = replace;\r\n        Index = index;\r\n    }\r\n\r\n    public string Key { get; }\r\n    public string Find { get; }\r\n    public string Replace { get; }\n\n    // only index 0 for now..\n    public int Index { get; }\r\n\r\n    public override string ToString() => $\"{nameof(HeaderFindAndReplace)}[{Key} at {Index}: {Find} -> {Replace}]\";\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/HttpHandlerOptions.cs",
    "content": "﻿using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\n/// <summary>\r\n/// Describes configuration parameters for http handler, that is created to handle a request to service.\r\n/// </summary>\r\npublic class HttpHandlerOptions //: SocketsHttpHandler // TODO Think about using inheritance or composition design since we initialize the SocketsHttpHandler instance with the options\r\n{\r\n    public const int DefaultPooledConnectionLifetimeSeconds = 120;\r\n\r\n    public HttpHandlerOptions()\r\n    {\r\n        MaxConnectionsPerServer = int.MaxValue;\r\n        PooledConnectionLifeTime = TimeSpan.FromSeconds(DefaultPooledConnectionLifetimeSeconds);\r\n    }\r\n\r\n    public HttpHandlerOptions(FileHttpHandlerOptions from)\r\n    {\r\n        AllowAutoRedirect = from.AllowAutoRedirect ?? false;\r\n        MaxConnectionsPerServer = from.MaxConnectionsPerServer.HasValue && from.MaxConnectionsPerServer.Value > 0\r\n            ? from.MaxConnectionsPerServer.Value : int.MaxValue;\r\n        PooledConnectionLifeTime = TimeSpan.FromSeconds(from.PooledConnectionLifetimeSeconds ?? DefaultPooledConnectionLifetimeSeconds);\r\n        UseCookieContainer = from.UseCookieContainer ?? false;\r\n        UseProxy = from.UseProxy ?? false;\r\n        UseTracing = from.UseTracing ?? false;\r\n    }\r\n\r\n    public HttpHandlerOptions(FileHttpHandlerOptions from, bool useTracing)\r\n        : this(from)\r\n    {\r\n        UseTracing = useTracing && (from.UseTracing ?? false);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Specify if auto redirect is enabled.\r\n    /// </summary>\r\n    /// <value>AllowAutoRedirect.</value>\r\n    public bool AllowAutoRedirect { get; init; }\r\n\r\n    /// <summary>\r\n    /// Specify is handler has to use a cookie container.\r\n    /// </summary>\r\n    /// <value>UseCookieContainer.</value>\r\n    public bool UseCookieContainer { get; init; }\r\n\r\n    /// <summary>\r\n    /// Specify is handler has to use a opentracing.\r\n    /// </summary>\r\n    /// <value>UseTracing.</value>\r\n    public bool UseTracing { get; init; }\r\n\r\n    /// <summary>\r\n    /// Specify if handler has to use a proxy.\r\n    /// </summary>\r\n    /// <value>UseProxy.</value>\r\n    public bool UseProxy { get; init; }\r\n\r\n    /// <summary>\r\n    /// Specify the maximum of concurrent connection to a network endpoint.\r\n    /// </summary>\r\n    /// <value>MaxConnectionsPerServer.</value>\r\n    public int MaxConnectionsPerServer { get; init; }\r\n\r\n    /// <summary>\r\n    /// Specify the maximum of time a connection can be pooled.\r\n    /// </summary>\r\n    /// <value>PooledConnectionLifeTime.</value>\r\n    public TimeSpan PooledConnectionLifeTime { get; init; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/IInternalConfiguration.cs",
    "content": "namespace Ocelot.Configuration;\r\n\r\npublic interface IInternalConfiguration\r\n{\r\n    string AdministrationPath { get; }\r\n    AuthenticationOptions AuthenticationOptions { get; }\r\n    CacheOptions CacheOptions { get; }\r\n    Version DownstreamHttpVersion { get; }\r\n    HttpVersionPolicy DownstreamHttpVersionPolicy { get; }\r\n    string DownstreamScheme { get; }\r\n    HttpHandlerOptions HttpHandlerOptions { get; }\r\n    LoadBalancerOptions LoadBalancerOptions { get; }\r\n    MetadataOptions MetadataOptions { get; }\r\n    QoSOptions QoSOptions { get; }\r\n    RateLimitOptions RateLimitOptions { get; }\r\n    string RequestId { get; }\r\n    Route[] Routes { get; }\r\n    ServiceProviderConfiguration ServiceProviderConfiguration { get; }\r\n    int? Timeout { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/InternalConfiguration.cs",
    "content": "using Ocelot.Configuration.File;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\npublic class InternalConfiguration : IInternalConfiguration\r\n{\r\n    public InternalConfiguration() => Routes = [];\r\n    public InternalConfiguration(Route[] routes) => Routes = routes ?? [];\r\n\r\n    public string AdministrationPath { get; init; }\r\n    public AuthenticationOptions AuthenticationOptions { get; init; }\r\n    public CacheOptions CacheOptions { get; set; }\r\n    public Version DownstreamHttpVersion { get; init; }\r\n\r\n    /// <summary>Global HTTP version policy. It is related to <see cref=\"FileRouteBase.DownstreamHttpVersionPolicy\"/> property.</summary>\r\n    /// <value>An <see cref=\"HttpVersionPolicy\"/> enumeration value.</value>\r\n    public HttpVersionPolicy DownstreamHttpVersionPolicy { get; init; }\n    public string DownstreamScheme { get; init; }\r\n    public HttpHandlerOptions HttpHandlerOptions { get; init; }\r\n    public LoadBalancerOptions LoadBalancerOptions { get; init; }\r\n    public MetadataOptions MetadataOptions { get; init; }\r\n    public QoSOptions QoSOptions { get; init; }\r\n    public RateLimitOptions RateLimitOptions { get; init; }\r\n    public string RequestId { get; init; }\r\n    public Route[] Routes { get; init; }\r\n    public ServiceProviderConfiguration ServiceProviderConfiguration { get; init; }\r\n    public int? Timeout { get; init; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/LoadBalancerOptions.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.LoadBalancer.Balancers;\n\nnamespace Ocelot.Configuration;\n\npublic class LoadBalancerOptions\n{\n    public LoadBalancerOptions()\n    {\n        Type = nameof(NoLoadBalancer);\n    }\n\n    public LoadBalancerOptions(FileLoadBalancerOptions options)\n        : this(options?.Type, options?.Key, options?.Expiry)\n    { }\n\n    public LoadBalancerOptions(string type, string key, int? expiryInMs)\n    {\n        Type = type.IfEmpty(nameof(NoLoadBalancer));\n        bool isStickySessions = nameof(CookieStickySessions).Equals(type, StringComparison.OrdinalIgnoreCase);\n        Key = isStickySessions\n            ? key.IfEmpty(CookieStickySessions.DefSessionCookieName)\n            : key;\n        ExpiryInMs = isStickySessions\n            ? expiryInMs ?? CookieStickySessions.DefSessionExpiryMilliseconds\n            : expiryInMs ?? 0;\n    }\n\n    public string Type { get; init; }\n    public string Key { get; init; }\n    public int ExpiryInMs { get; init; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/MetadataOptions.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing System.Globalization;\n\nnamespace Ocelot.Configuration;\n\npublic class MetadataOptions\n{\n    public MetadataOptions()\n    {\n        CurrentCulture = CultureInfo.CurrentCulture;\n        NumberStyle = NumberStyles.Any;\n        Separators = new[] { \",\" };\n        StringSplitOption = StringSplitOptions.None;\n        TrimChars = new[] { ' ' };\n        Metadata = new Dictionary<string, string>();\n    }\n\n    public MetadataOptions(MetadataOptions from)\n    {\n        CurrentCulture = from.CurrentCulture;\n        NumberStyle = from.NumberStyle;\n        Separators = from.Separators;\n        StringSplitOption = from.StringSplitOption;\n        TrimChars = from.TrimChars;\n        Metadata = from.Metadata;\n    }\n\n    public MetadataOptions(FileMetadataOptions from)\n    {\n        CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture);\n        NumberStyle = Enum.Parse<NumberStyles>(from.NumberStyle);\n        Separators = from.Separators;\n        StringSplitOption = Enum.Parse<StringSplitOptions>(from.StringSplitOption);\n        TrimChars = from.TrimChars;\n        Metadata = new Dictionary<string, string>();\n    }\n\n    public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption,\n        NumberStyles numberStyle, CultureInfo currentCulture, IDictionary<string, string> metadata)\n    {\n        CurrentCulture = currentCulture;\n        NumberStyle = numberStyle;\n        Separators = separators;\n        StringSplitOption = stringSplitOption;\n        TrimChars = trimChars;\n        Metadata = metadata;\n    }\n\n    public CultureInfo CurrentCulture { get; }\n    public NumberStyles NumberStyle { get; }\n    public string[] Separators { get; }\n    public StringSplitOptions StringSplitOption { get; }\n    public char[] TrimChars { get; }\n    public IDictionary<string, string> Metadata { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs",
    "content": "﻿using Ocelot.Infrastructure;\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Configuration.Parser;\r\n\r\n/// <summary>\r\n/// Default implementation of the <see cref=\"IClaimToThingConfigurationParser\"/> interface.\r\n/// </summary>\r\npublic partial class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser\r\n{\r\n    private const char SplitToken = '>';\r\n\r\n    [GeneratedRegex(\"Claims\\\\[.*\\\\]\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex ClaimRegex();\r\n\r\n    [GeneratedRegex(\"value\\\\[.*\\\\]\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex IndexRegex();\r\n\r\n    public Response<ClaimToThing> Extract(string existingKey, string value)\r\n    {\r\n        try\r\n        {\r\n            var instructions = value.Split(SplitToken);\r\n\r\n            if (instructions.Length <= 1)\r\n            {\r\n                return new ErrorResponse<ClaimToThing>(new NoInstructionsError(SplitToken.ToString()));\r\n            }\r\n\r\n            var claimMatch = ClaimRegex().IsMatch(instructions[0]);\r\n\r\n            if (!claimMatch)\r\n            {\r\n                return new ErrorResponse<ClaimToThing>(new InstructionNotForClaimsError());\r\n            }\r\n\r\n            var newKey = GetIndexValue(instructions[0]);\r\n            var index = 0;\r\n            var delimiter = string.Empty;\r\n\r\n            if (instructions.Length > 2 && IndexRegex().IsMatch(instructions[1]))\r\n            {\r\n                index = int.Parse(GetIndexValue(instructions[1]));\r\n                delimiter = instructions[2].Trim();\r\n            }\r\n\r\n            return new OkResponse<ClaimToThing>(\r\n                           new ClaimToThing(existingKey, newKey, delimiter, index));\r\n        }\r\n        catch (Exception exception)\r\n        {\r\n            return new ErrorResponse<ClaimToThing>(new ParsingConfigurationHeaderError(exception));\r\n        }\r\n    }\r\n\r\n    private static string GetIndexValue(string instruction)\r\n    {\r\n        var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal);\r\n        var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal);\r\n        var length = lastIndexer - firstIndexer;\r\n        var claimKey = instruction.Substring(firstIndexer + 1, length - 1);\r\n        return claimKey;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs",
    "content": "using Ocelot.Responses;\r\n\r\nnamespace Ocelot.Configuration.Parser;\r\n\r\npublic interface IClaimToThingConfigurationParser\r\n{\r\n    Response<ClaimToThing> Extract(string existingKey, string value);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Configuration.Parser;\n\npublic class InstructionNotForClaimsError : Error\n{\n    public InstructionNotForClaimsError()\n        : base(\"instructions did not contain claims, at the moment we only support claims extraction\", OcelotErrorCode.InstructionNotForClaimsError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Parser/NoInstructionsError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Configuration.Parser;\n\npublic class NoInstructionsError : Error\n{\n    public NoInstructionsError(string splitToken)\n        : base($\"There we no instructions splitting on {splitToken}\", OcelotErrorCode.NoInstructionsError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.Configuration.Parser;\n\npublic class ParsingConfigurationHeaderError : Error\n{\n    public ParsingConfigurationHeaderError(Exception exception)\n        : base($\"Parsing configuration exception is {exception.Message}\",\n            OcelotErrorCode.ParsingConfigurationHeaderError,\n            StatusCodes.Status404NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/QoSOptions.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration;\n\npublic class QoSOptions\n{\n    public QoSOptions() { }\n    public QoSOptions(int? timeout) => Timeout = timeout;\n    public QoSOptions(int? exceptions, int? breakMs)\n    {\n        BreakDuration = breakMs;\n        MinimumThroughput = exceptions;\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"QoSOptions\"/> class.</summary>\n    /// <remarks>This is the copying constructor.</remarks>\n    /// <param name=\"from\">The object to copy the properties from.</param>\n    public QoSOptions(QoSOptions from)\n    {\n        BreakDuration = from.BreakDuration;\n        MinimumThroughput = from.MinimumThroughput;\n        FailureRatio = from.FailureRatio;\n        SamplingDuration = from.SamplingDuration;\n        Timeout = from.Timeout;\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"QoSOptions\"/> class from a <see cref=\"FileQoSOptions\"/> model.</summary>\n    /// <remarks>This is the converting constructor.</remarks>\n    /// <param name=\"from\">The File-model to copy the properties from.</param>\n    public QoSOptions(FileQoSOptions from)\n    {\n        BreakDuration = from.DurationOfBreak ?? from.BreakDuration;\n        MinimumThroughput = from.ExceptionsAllowedBeforeBreaking ?? from.MinimumThroughput;\n        FailureRatio = from.FailureRatio;\n        SamplingDuration = from.SamplingDuration;\n        Timeout = from.TimeoutValue ?? from.Timeout;\n    }\n\n    /// <summary>Gets the duration, in milliseconds, that the circuit remains open before resetting.</summary>\n    /// <remarks>Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class.</remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value (milliseconds).</value>\n    public int? BreakDuration { get; init; }\n\n    /// <summary>Gets the minimum number of failures required before the circuit is set to open.</summary>\n    /// <remarks>Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class.</remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value (exceptions number).</value>\n    public int? MinimumThroughput { get; init; }\n\n    /// <summary>Gets or sets the failure-to-success ratio at which the circuit will break.</summary>\n    /// <remarks>Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class.</remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"double\"/>) value.</value>\n    public double? FailureRatio { get; init; }\n\n    /// <summary>Gets or sets the milliseconds duration of the sampling over which <see cref=\"FailureRatio\"/> is assessed.</summary>\n    /// <remarks>Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the TimeoutStrategy class.</remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value (milliseconds).</value>\n    public int? SamplingDuration { get; init; }\n\n    /// <summary>Gets the timeout in milliseconds.</summary>\n    /// <remarks>Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the TimeoutStrategy class.</remarks>\n    /// <value>A <see cref=\"Nullable{T}\"/> (T is <see cref=\"int\"/>) value (milliseconds).</value>\n    public int? Timeout { get; init; }\n\n    public bool UseQos => (MinimumThroughput.HasValue && MinimumThroughput > 0)\n        || (Timeout.HasValue && Timeout > 0);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/RateLimitOptions.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Microsoft.Extensions.Caching.Distributed;\r\nusing Microsoft.Extensions.Caching.Memory;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure.Extensions;\r\nusing Ocelot.RateLimiting;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\n/// <summary>\r\n/// RateLimit Options.\r\n/// </summary>\r\npublic class RateLimitOptions\r\n{\r\n    public const string DefaultClientHeader = \"Oc-Client\";\r\n    public static readonly string DefaultCounterPrefix = typeof(RateLimiting.RateLimiting).Namespace;\r\n    public const int DefaultStatus429 = StatusCodes.Status429TooManyRequests;\r\n    public const string DefaultQuotaMessage = \"API calls quota exceeded! Maximum admitted {0} per {1}.\";\r\n\r\n    public RateLimitOptions()\r\n    {\r\n        ClientIdHeader = DefaultClientHeader;\r\n        ClientWhitelist = [];\r\n        EnableHeaders = true;\r\n        EnableRateLimiting = true;\r\n        StatusCode = DefaultStatus429;\r\n        QuotaMessage = DefaultQuotaMessage;\r\n        KeyPrefix = DefaultCounterPrefix;\r\n        Rule = RateLimitRule.Empty;\r\n    }\r\n\r\n    public RateLimitOptions(bool enableRateLimiting) : this()\r\n    {\r\n        EnableRateLimiting = enableRateLimiting;\r\n    }\r\n\r\n    public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IList<string> clientWhitelist, bool enableHeaders,\r\n        string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)\r\n    {\r\n        ClientIdHeader = clientIdHeader.IfEmpty(DefaultClientHeader);\r\n        ClientWhitelist = clientWhitelist ?? [];\r\n        EnableHeaders = enableHeaders;\r\n        EnableRateLimiting = enableRateLimiting;\r\n        KeyPrefix = rateLimitCounterPrefix.IfEmpty(DefaultCounterPrefix);\r\n        QuotaMessage = quotaExceededMessage.IfEmpty(DefaultQuotaMessage);\r\n        Rule = rateLimitRule;\r\n        StatusCode = httpStatusCode;\r\n    }\r\n\r\n    public RateLimitOptions(FileRateLimitByHeaderRule fromRule)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(fromRule);\r\n\r\n        ClientIdHeader = fromRule.ClientIdHeader.IfEmpty(DefaultClientHeader);\r\n        ClientWhitelist = fromRule.ClientWhitelist ?? [];\r\n        EnableHeaders = fromRule.DisableRateLimitHeaders.HasValue ? !fromRule.DisableRateLimitHeaders.Value\r\n            : fromRule.EnableHeaders ?? true;\r\n        EnableRateLimiting = fromRule.EnableRateLimiting ?? true;\r\n        StatusCode = fromRule.HttpStatusCode ?? fromRule.StatusCode ?? DefaultStatus429;\r\n        QuotaMessage = fromRule.QuotaExceededMessage.IfEmpty(fromRule.QuotaMessage.IfEmpty(DefaultQuotaMessage));\r\n        KeyPrefix = fromRule.RateLimitCounterPrefix.IfEmpty(fromRule.KeyPrefix.IfEmpty(DefaultCounterPrefix));\r\n        Rule = new(\r\n            fromRule.Period.IfEmpty(RateLimitRule.DefaultPeriod),\r\n            fromRule.PeriodTimespan.HasValue ? $\"{fromRule.PeriodTimespan.Value}s\" : fromRule.Wait,\r\n            fromRule.Limit ?? RateLimitRule.ZeroLimit);\r\n    }\r\n\r\n    public RateLimitOptions(RateLimitOptions fromOptions)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(fromOptions);\r\n\r\n        ClientIdHeader = fromOptions.ClientIdHeader.IfEmpty(DefaultClientHeader);\r\n        ClientWhitelist = fromOptions.ClientWhitelist ?? [];\r\n        EnableHeaders = fromOptions.EnableHeaders;\r\n        EnableRateLimiting = fromOptions.EnableRateLimiting;\r\n        StatusCode = fromOptions.StatusCode;\r\n        QuotaMessage = fromOptions.QuotaMessage.IfEmpty(DefaultQuotaMessage);\r\n        KeyPrefix = fromOptions.KeyPrefix.IfEmpty(DefaultCounterPrefix);\r\n        Rule = fromOptions.Rule ?? RateLimitRule.Empty;\r\n    }\r\n\r\n    /// <summary>Gets a Rate Limit rule.</summary>\r\n    /// <value>A <see cref=\"RateLimitRule\"/> object that represents the rule.</value>\r\n    public RateLimitRule Rule { get; init; }\n\n    /// <summary>A list of approved clients aka whitelisted ones.</summary>\r\n    /// <value>An <see cref=\"IList{T}\"/> collection of allowed clients.</value>\r\n    public IList<string> ClientWhitelist { get; init; }\n\n    /// <summary>Gets or sets the HTTP header used to store the client identifier, which defaults to <c>Oc-Client</c>.</summary>\r\n    /// <value>A <see cref=\"string\"/> representing the name of the HTTP header.</value>\r\n    public string ClientIdHeader { get; init; }\n\n    /// <summary>Gets or sets the rejection status code returned during the Quota Exceeded period, aka the <see cref=\"RateLimitRule.Wait\"/> window, or the remainder of the <see cref=\"RateLimitRule.Period\"/> fixed window following the moment of exceeding.\r\n    /// <para>Default value: <see href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429\">429 (Too Many Requests)</see>.</para></summary>\r\n    /// <value>A <see cref=\"int\"/> value.</value>\r\n    public int StatusCode { get; init; }\n\n    /// <summary>\r\n    /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message.\r\n    /// <para>If none specified the default will be: <see cref=\"DefaultQuotaMessage\"/>.</para>\r\n    /// </summary>\r\n    /// <value>A <see cref=\"string\"/> value that will be used as a formatter.</value>\r\n    public string QuotaMessage { get; init; }\n\n    /// <summary>Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the <see cref=\"IRateLimitStorage\"/> service.</summary>\r\n    /// <remarks>Notes:\r\n    /// <list type=\"number\">\r\n    /// <item>The consumer is the <see cref=\"IRateLimiting.GetStorageKey(ClientRequestIdentity, RateLimitOptions)\"/> method.</item>\r\n    /// <item>The property is relevant for distributed storage systems, such as <see cref=\"IDistributedCache\"/> services, to inform users about which objects are being cached for management purposes.\r\n    /// By default, each Ocelot instance uses its own <see cref=\"IMemoryCache\"/> service without cross-instance synchronization.</item>\r\n    /// </list>\r\n    /// </remarks>\r\n    /// <value>A <see cref=\"string\"/> object which value defaults to \"Ocelot.RateLimiting\", see the <see cref=\"DefaultCounterPrefix\"/> property.</value>\r\n    public string KeyPrefix { get; init; }\r\n\n    /// <summary>Enables or disables rate limiting. Defaults to <see langword=\"true\"/> (enabled).</summary>\r\n    /// <value>A <see langword=\"bool\"/> value.</value>\r\n    public bool EnableRateLimiting { get; init; }\n\n    /// <summary>Enables or disables <c>X-RateLimit-*</c> and <c>Retry-After</c> headers.</summary>\r\n    /// <value>A <see cref=\"bool\"/> value.</value>\r\n    public bool EnableHeaders { get; init; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/RateLimitRule.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\r\nusing System.Globalization;\r\n\r\nnamespace Ocelot.Configuration;\r\n\r\npublic class RateLimitRule\r\n{\r\n    public const string DefaultPeriod = \"1s\";\r\n    public const string ZeroWait = \"0ms\";\r\n    public const long ZeroLimit = 0L;\r\n    public static RateLimitRule Empty = new(DefaultPeriod, ZeroWait, ZeroLimit);\r\n\r\n    public RateLimitRule(string period, string wait, long limit)\r\n    {\r\n        Period = period.IfEmpty(DefaultPeriod);\r\n        Wait = wait.IfEmpty(ZeroWait);\r\n        Limit = Math.Abs(limit);\r\n    }\r\n\r\n    public override string ToString() => $\"{Limit}/{Period}/w{Wait}\";\r\n\n    /// <summary>\r\n    /// Rate limiting durations can be set using units like 'ms' (milliseconds), 's' (seconds), 'm' (minutes), 'h' (hours), or 'd' (days).\r\n    /// </summary>\r\n    /// <value>A <see cref=\"string\"/> object with the period (fixed window).</value>\r\n    public string Period { get; }\r\n\r\n    /// <summary>A processed form of the <see cref=\"Period\"/> property optimized for quick algorithm computations.</summary>\r\n    /// <value>A <see cref=\"TimeSpan\"/> value.</value>\r\n    public TimeSpan PeriodSpan { get => _periodSpan ??= ParseTimespan(Period); }\n    private TimeSpan? _periodSpan;\n\n    /// <summary>\n    /// Wait window after exceeding the rate limit, which has 'ms', 's', 'm', 'h', 'd' units.\n    /// </summary>\r\n    /// <value>A <see cref=\"string\"/> object with the waiting window.</value>\r\n    public string Wait { get; }\n\r\n    /// <summary>A processed form of the <see cref=\"Wait\"/> property optimized for quick algorithm computations.</summary>\r\n    /// <value>A <see cref=\"TimeSpan\"/> value.</value>\r\n    public TimeSpan WaitSpan { get => _waitSpan ??= ParseTimespan(Wait); }\n    private TimeSpan? _waitSpan;\n\n    /// <summary>\r\n    /// Maximum number of requests that a client can make in a defined period.\r\n    /// </summary>\r\n    /// <value>A <see cref=\"long\"/> value with maximum number of requests.</value>\r\n    public long Limit { get; }\r\n\r\n    /// <summary>\r\n    /// Parses a timespan string, such as \"1ms\", \"1s\", \"1m\", \"1h\", \"1d\".\r\n    /// </summary>\r\n    /// <remarks>Converts a string to milliseconds when the unit is missing or undefined, automatically applying the 'ms' unit.</remarks>\r\n    /// <param name=\"timespan\">The string value with units: '1ms', '1s', '1m', '1h', '1d'.</param>\r\n    /// <returns>A <see cref=\"TimeSpan\"/> value.</returns>\r\n    /// <exception cref=\"FormatException\">If the value is not a number, or the unit of value cannot be determined.</exception>\r\n    public static TimeSpan ParseTimespan(string timespan)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(timespan))\r\n        {\r\n            return TimeSpan.Zero;\r\n        }\r\n\r\n        if (!timespan.Any(char.IsDigit)) \r\n        {\r\n            // TODO: Make sense to have validation in src/Ocelot/Configuration/Validator/RouteFluentValidator\r\n            throw new FormatException($\"The '{timespan}' value doesn't include any digits, so it cannot be considered a number!\");\r\n        }\r\n\r\n        string val = timespan.Trim();\r\n        int pos = val.Length;\r\n        while (--pos >= 0 && !char.IsDigit(val[pos]) && val[pos] != DecimalSeparator)\r\n        {\r\n        }\r\n\r\n        string floating = val[..++pos], unit = val[pos..];\r\n        double value = Math.Abs(double.Parse(floating)); // negative values should be disallowed as they could cause everything to malfunction; TODO: Make sense to have validation in src/Ocelot/Configuration/Validator/RouteFluentValidator\r\n        return unit.ToLower() switch\r\n        {\r\n            \"d\" => TimeSpan.FromDays(value),\r\n            \"h\" => TimeSpan.FromHours(value),\r\n            \"m\" => TimeSpan.FromMinutes(value),\r\n            \"s\" => TimeSpan.FromSeconds(value),\r\n            \"ms\" => TimeSpan.FromMilliseconds(value),\r\n            \"\" => TimeSpan.FromMilliseconds(value), // an unknown unit defaults to milliseconds as the ms unit\r\n            _ => throw new FormatException($\"The '{timespan}' timespan cannot be converted to {nameof(TimeSpan)} due to an unknown '{unit}' unit!\"),\r\n        };\r\n    }\r\n\r\n    private static readonly char DecimalSeparator = new NumberFormatInfo().NumberDecimalSeparator[0];\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs",
    "content": "﻿namespace Ocelot.Configuration.Repository;\n\npublic class ConsulFileConfigurationPollerOption : IFileConfigurationPollerOptions\n{\n    private readonly IInternalConfigurationRepository _internalConfigRepo;\n    private readonly IFileConfigurationRepository _fileConfigurationRepository;\n\n    public ConsulFileConfigurationPollerOption(IInternalConfigurationRepository internalConfigurationRepository,\n                                               IFileConfigurationRepository fileConfigurationRepository)\n    {\n        _internalConfigRepo = internalConfigurationRepository;\n        _fileConfigurationRepository = fileConfigurationRepository;\n    }\n\n    public int Delay => GetDelay();\n\n    private int GetDelay()\n    {\n        var delay = 1000;\n\n        var fileConfig = _fileConfigurationRepository.Get().GetAwaiter().GetResult(); // sync call, so TODO extend IFileConfigurationPollerOptions interface with 2nd async method\n        if (fileConfig?.Data?.GlobalConfiguration?.ServiceDiscoveryProvider != null &&\n                !fileConfig.IsError &&\n                fileConfig.Data.GlobalConfiguration.ServiceDiscoveryProvider.PollingInterval > 0)\n        {\n            delay = fileConfig.Data.GlobalConfiguration.ServiceDiscoveryProvider.PollingInterval;\n        }\n        else\n        {\n            var internalConfig = _internalConfigRepo.Get();\n            if (internalConfig?.Data?.ServiceProviderConfiguration != null &&\n            !internalConfig.IsError &&\n            internalConfig.Data.ServiceProviderConfiguration.PollingInterval > 0)\n            {\n                delay = internalConfig.Data.ServiceProviderConfiguration.PollingInterval;\n            }\n        }\n\n        return delay;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs",
    "content": "using Microsoft.AspNetCore.Hosting;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Responses;\nusing FileSys = System.IO.File;\n\nnamespace Ocelot.Configuration.Repository;\n\npublic class DiskFileConfigurationRepository : IFileConfigurationRepository\n{\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;\n    private FileInfo _ocelotFile;\n    private FileInfo _environmentFile;\n    private readonly object _lock = new();\n\n    public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource)\n    {\n        _hostingEnvironment = hostingEnvironment;\n        _changeTokenSource = changeTokenSource;\n        Initialize(AppContext.BaseDirectory);\n    }\n\n    public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource, string folder)\n    {\n        _hostingEnvironment = hostingEnvironment;\n        _changeTokenSource = changeTokenSource;\n        Initialize(folder);\n    }\n\n    private void Initialize(string folder)\n    {\n        folder ??= AppContext.BaseDirectory;\n        _ocelotFile = new FileInfo(Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile));\n        var envFile = !string.IsNullOrEmpty(_hostingEnvironment.EnvironmentName)\n            ? string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, _hostingEnvironment.EnvironmentName)\n            : ConfigurationBuilderExtensions.PrimaryConfigFile;\n        _environmentFile = new FileInfo(Path.Combine(folder, envFile));\n    }\n\n    public Task<Response<FileConfiguration>> Get()\n    {\n        string jsonConfiguration;\n\n        lock (_lock)\n        {\n            jsonConfiguration = FileSys.ReadAllText(_environmentFile.FullName);\n        }\n\n        var fileConfiguration = JsonConvert.DeserializeObject<FileConfiguration>(jsonConfiguration);\n\n        return Task.FromResult<Response<FileConfiguration>>(new OkResponse<FileConfiguration>(fileConfiguration));\n    }\n\n    public Task<Response> Set(FileConfiguration fileConfiguration)\n    {\n        var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);\n\n        lock (_lock)\n        {\n            if (_environmentFile.Exists)\n            {\n                _environmentFile.Delete();\n            }\n\n            FileSys.WriteAllText(_environmentFile.FullName, jsonConfiguration);\n\n            if (_ocelotFile.Exists)\n            {\n                _ocelotFile.Delete();\n            }\n\n            FileSys.WriteAllText(_ocelotFile.FullName, jsonConfiguration);\n        }\n\n        _changeTokenSource.Activate();\n        return Task.FromResult<Response>(new OkResponse());\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs",
    "content": "using Microsoft.Extensions.Hosting;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\n\nnamespace Ocelot.Configuration.Repository;\n\npublic class FileConfigurationPoller : IHostedService, IDisposable\n{\n    private readonly IOcelotLogger _logger;\n    private readonly IFileConfigurationRepository _repo;\n    private string _previousAsJson;\n    private Timer _timer;\n    private volatile bool _polling;\n    private readonly IFileConfigurationPollerOptions _options;\n    private readonly IInternalConfigurationRepository _internalConfigRepo;\n    private readonly IInternalConfigurationCreator _internalConfigCreator;\n\n    public FileConfigurationPoller(\n        IOcelotLoggerFactory factory,\n        IFileConfigurationRepository repo,\n        IFileConfigurationPollerOptions options,\n        IInternalConfigurationRepository internalConfigRepo,\n        IInternalConfigurationCreator internalConfigCreator)\n    {\n        _internalConfigRepo = internalConfigRepo;\n        _internalConfigCreator = internalConfigCreator;\n        _options = options;\n        _logger = factory.CreateLogger<FileConfigurationPoller>();\n        _repo = repo;\n        _previousAsJson = string.Empty;\n    }\n\n    private void OnTimer(object state)\n    {\n        if (_polling)\n            return;\n\n        _polling = true;\n        PollAsync().GetAwaiter().GetResult(); // TODO This is not good, TimerCallback must be synchronous\n        _polling = false;\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        if (_timer is not null)\n            return Task.CompletedTask;\n\n        _logger.LogInformation(() => $\"{nameof(FileConfigurationPoller)} is starting.\");\n        _timer = new(OnTimer, null, _options.Delay, _options.Delay); // TODO state could be CancellationToken?\n        return Task.CompletedTask;\n    }\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_timer is null)\n            return Task.CompletedTask;\n\n        _logger.LogInformation(() => $\"{nameof(FileConfigurationPoller)} is stopping.\");\n        _timer.Change(Timeout.Infinite, 0);\n        return Task.CompletedTask;\n    }\n\n    private async Task PollAsync()\n    {\n        _logger.LogInformation(() => $\"{nameof(PollAsync)}: Started polling\");\n\n        var fileConfig = await _repo.Get();\n        if (fileConfig.IsError)\n        {\n            _logger.LogWarning(() => $\"{nameof(PollAsync)}: Error getting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}\");\n            return;\n        }\n\n        var asJson = ToJson(fileConfig.Data);\n        if (asJson != _previousAsJson)\n        {\n            var config = await _internalConfigCreator.Create(fileConfig.Data);\n            if (!config.IsError)\n                _internalConfigRepo.AddOrReplace(config.Data);\n\n            _previousAsJson = asJson;\n        }\n\n        _logger.LogInformation(() => $\"{nameof(PollAsync)}: Finished polling\");\n    }\n\n    /// <summary>\n    /// We could do object comparison here but performance isnt really a problem. This might be an issue one day!.\n    /// </summary>\n    /// <returns>hash of the config.</returns>\n    private static string ToJson(FileConfiguration config)\n    {\n        var currentHash = JsonConvert.SerializeObject(config);\n        return currentHash;\n    }\n\n    public void Dispose()\n    {\n        _timer?.Dispose();\n        _timer = null;\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/IFileConfigurationPollerOptions.cs",
    "content": "﻿namespace Ocelot.Configuration.Repository;\n\npublic interface IFileConfigurationPollerOptions\n{\n    int Delay { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs",
    "content": "using Ocelot.Configuration.File;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Configuration.Repository;\r\n\r\npublic interface IFileConfigurationRepository\r\n{\r\n    Task<Response<FileConfiguration>> Get();\r\n\r\n    Task<Response> Set(FileConfiguration fileConfiguration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/IInternalConfigurationRepository.cs",
    "content": "﻿using Ocelot.Responses;\n\nnamespace Ocelot.Configuration.Repository;\n\npublic interface IInternalConfigurationRepository\n{\n    Response<IInternalConfiguration> Get();\n\n    Response AddOrReplace(IInternalConfiguration internalConfiguration);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/InMemoryFileConfigurationPollerOptions.cs",
    "content": "namespace Ocelot.Configuration.Repository;\n\npublic class InMemoryFileConfigurationPollerOptions : IFileConfigurationPollerOptions\n{\n    public int Delay => 1000;\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs",
    "content": "﻿using Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Configuration.Repository;\n\n/// <summary>\n/// Register as singleton.\n/// </summary>\npublic class InMemoryInternalConfigurationRepository : IInternalConfigurationRepository\n{\n    private static readonly object LockObject = new();\n\n    private IInternalConfiguration _internalConfiguration;\n    private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;\n\n    public InMemoryInternalConfigurationRepository(IOcelotConfigurationChangeTokenSource changeTokenSource)\n    {\n        _changeTokenSource = changeTokenSource;\n    }\n\n    public Response<IInternalConfiguration> Get()\n    {\n        return new OkResponse<IInternalConfiguration>(_internalConfiguration);\n    }\n\n    public Response AddOrReplace(IInternalConfiguration internalConfiguration)\n    {\n        lock (LockObject)\n        {\n            _internalConfiguration = internalConfiguration;\n        }\n\n        _changeTokenSource.Activate();\n        return new OkResponse();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Route.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Values;\n\nnamespace Ocelot.Configuration;\n\npublic class Route\n{\n    public Route() => DownstreamRoute = new();\n    public Route(bool isDynamic) : this() => IsDynamic = isDynamic;\n    public Route(bool isDynamic, DownstreamRoute route) : this(route) => IsDynamic = isDynamic;\n    public Route(DownstreamRoute route) => DownstreamRoute = [route];\n    public Route(DownstreamRoute route, HttpMethod method)\n    {\n        DownstreamRoute = [route];\n        UpstreamHttpMethod = [method];\n    }\n\n    public bool IsDynamic { get; }\n    public string Aggregator { get; init; }\n    public List<DownstreamRoute> DownstreamRoute { get; init; }\n    public List<AggregateRouteConfig> DownstreamRouteConfig { get; init; }\n    public IDictionary<string, UpstreamHeaderTemplate> UpstreamHeaderTemplates { get; init; }\n    public string UpstreamHost { get; init; }\n    public HashSet<HttpMethod> UpstreamHttpMethod { get; init; }\n    public UpstreamPathTemplate UpstreamTemplatePattern { get; init; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/SecurityOptions.cs",
    "content": "﻿namespace Ocelot.Configuration;\n\npublic class SecurityOptions\n{\n    public SecurityOptions()\n    {\n        IPAllowedList = new List<string>();\n        IPBlockedList = new List<string>();\n    }\n\n    public SecurityOptions(string allowed = null, string blocked = null)\n        : this()\n    {\n        if (!string.IsNullOrEmpty(allowed))\n        {\n            IPAllowedList.Add(allowed);\n        }\n\n        if (!string.IsNullOrEmpty(blocked))\n        {\n            IPBlockedList.Add(blocked);\n        }\n    }\n\n    public SecurityOptions(IList<string> allowedList = null, IList<string> blockedList = null)\n    {\n        IPAllowedList = allowedList ?? new List<string>();\n        IPBlockedList = blockedList ?? new List<string>();\n    }\n\n    public IList<string> IPAllowedList { get; }\n    public IList<string> IPBlockedList { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/ServiceProviderConfiguration.cs",
    "content": "﻿namespace Ocelot.Configuration;\n\npublic class ServiceProviderConfiguration\n{\n    public string Scheme { get; init; }\n\n    public string Host { get; init; }\n\n    public int Port { get; init; }\n\n    public string Type { get; init; }\n\n    public string Token { get; init; }\n\n    public string ConfigurationKey { get; init; }\n\n    public int PollingInterval { get; init; }\n\n    public string Namespace { get; init; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs",
    "content": "using Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Configuration.Setter;\n\npublic class FileAndInternalConfigurationSetter : IFileConfigurationSetter\n{\n    private readonly IInternalConfigurationRepository _internalConfigRepo;\n    private readonly IInternalConfigurationCreator _configCreator;\n    private readonly IFileConfigurationRepository _repo;\n\n    public FileAndInternalConfigurationSetter(\n        IInternalConfigurationRepository configRepo,\n        IInternalConfigurationCreator configCreator,\n        IFileConfigurationRepository repo)\n    {\n        _internalConfigRepo = configRepo;\n        _configCreator = configCreator;\n        _repo = repo;\n    }\n\n    public async Task<Response> Set(FileConfiguration fileConfig)\n    {\n        var response = await _repo.Set(fileConfig);\n\n        if (response.IsError)\n        {\n            return new ErrorResponse(response.Errors);\n        }\n\n        var config = await _configCreator.Create(fileConfig);\n\n        if (!config.IsError)\n        {\n            _internalConfigRepo.AddOrReplace(config.Data);\n        }\n\n        return new ErrorResponse(config.Errors);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Configuration.Setter;\n\npublic interface IFileConfigurationSetter\n{\n    Task<Response> Set(FileConfiguration config);\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Configuration.Validator;\n\npublic class ConfigurationValidationResult\n{\n    public ConfigurationValidationResult(bool isError)\n    {\n        IsError = isError;\n        Errors = new List<Error>();\n    }\n\n    public ConfigurationValidationResult(bool isError, List<Error> errors)\n    {\n        IsError = isError;\n        Errors = errors;\n    }\n\n    public bool IsError { get; }\n\n    public List<Error> Errors { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/FileAuthenticationOptionsValidator.cs",
    "content": "﻿using FluentValidation;\nusing Microsoft.AspNetCore.Authentication;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.Configuration.Validator;\n\npublic class FileAuthenticationOptionsValidator : AbstractValidator<FileAuthenticationOptions>\n{\n    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;\n\n    public FileAuthenticationOptionsValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)\n    {\n        _authenticationSchemeProvider = authenticationSchemeProvider;\n\n        RuleFor(authOptions => authOptions)\n            .MustAsync(IsSupportedAuthenticationProviders)\n            .WithMessage($\"{nameof(FileRoute.AuthenticationOptions)}: {{PropertyValue}} is unsupported authentication provider\");\n    }\n\n    private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)\n    {\n        var keys = options.AuthenticationProviderKeys;\n        if (options.AuthenticationProviderKey.IsEmpty() && (keys is null || keys.Length == 0))\n        {\n            return true;\n        }\n\n        var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();\n        var supportedSchemes = schemes.Select(scheme => scheme.Name);\n        var primary = options.AuthenticationProviderKey;\n        return !string.IsNullOrWhiteSpace(primary) && supportedSchemes.Contains(primary)\n            || (string.IsNullOrWhiteSpace(primary) && keys.All(supportedSchemes.Contains));\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs",
    "content": "﻿using FluentValidation;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.File;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\n\r\nnamespace Ocelot.Configuration.Validator;\r\n\r\n/// <summary>Validation of a <see cref=\"FileConfiguration\"/> objects.</summary>\npublic partial class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator\r\n{\r\n    private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates;\n\r\n    public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator)\r\n    {\r\n        _serviceDiscoveryFinderDelegates = provider\r\n            .GetServices<ServiceDiscoveryFinderDelegate>()\r\n            .ToList();\r\n\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .SetValidator(routeFluentValidator);\r\n\r\n        RuleFor(configuration => configuration.GlobalConfiguration)\r\n            .SetValidator(fileGlobalConfigurationFluentValidator);\r\n\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .Must((config, route) => IsNotDuplicateIn(route, config.Routes))\r\n            .WithMessage((_, route) => $\"{nameof(route)} {route.UpstreamPathTemplate} has duplicate\");\r\n\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .Must((config, route) => HaveServiceDiscoveryProviderRegistered(route, config.GlobalConfiguration.ServiceDiscoveryProvider))\r\n            .WithMessage((_, _) => \"Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?\");\r\n\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.UpstreamPathTemplate))\r\n            .WithMessage((_, route) => $\"{nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}' has duplicated placeholder\");\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.DownstreamPathTemplate))\r\n            .WithMessage((_, route) => $\"{nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}' has duplicated placeholder\");\n\r\n        RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider)\r\n            .Must(HaveServiceDiscoveryProviderRegistered)\r\n            .WithMessage((_, _) => \"Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?\");\r\n\r\n        RuleForEach(configuration => configuration.Routes)\r\n            .Must((config, route) => IsNotDuplicateIn(route, config.Aggregates))\r\n            .WithMessage((_, route) => $\"{nameof(route)} {route.UpstreamPathTemplate} has duplicate aggregate\");\r\n\r\n        RuleForEach(configuration => configuration.Aggregates)\r\n            .Must((config, aggregateRoute) => IsNotDuplicateIn(aggregateRoute, config.Aggregates))\r\n            .WithMessage((_, aggregate) => $\"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate\");\r\n\r\n        RuleForEach(configuration => configuration.Aggregates)\r\n            .Must((config, aggregateRoute) => AllRoutesForAggregateExist(aggregateRoute, config.Routes))\r\n            .WithMessage((_, aggregateRoute) => $\"Routes for {nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property\");\r\n\r\n        RuleForEach(configuration => configuration.Aggregates)\r\n            .Must((config, aggregateRoute) => DoesNotContainRoutesWithSpecificRequestIdKeys(aggregateRoute, config.Routes))\r\n            .WithMessage((_, aggregateRoute) => $\"{nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} contains Route with specific RequestIdKey, this is not possible with Aggregates\");\r\n    }\r\n\r\n    private const string ServiceFabric = ServiceFabricServiceDiscoveryProvider.Type;\r\n    private bool HaveServiceDiscoveryProviderRegistered(FileRoute route, FileServiceDiscoveryProvider serviceDiscoveryProvider)\r\n    {\r\n        return string.IsNullOrEmpty(route.ServiceName) ||\r\n               ServiceFabric.Equals(serviceDiscoveryProvider?.Type, StringComparison.OrdinalIgnoreCase) ||\r\n               _serviceDiscoveryFinderDelegates.Any();\r\n    }\r\n\r\n    private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider)\r\n    {\r\n        return serviceDiscoveryProvider == null ||\n            ServiceFabric.Equals(serviceDiscoveryProvider.Type, StringComparison.OrdinalIgnoreCase) ||\n            string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any();\r\n    }\r\n\r\n    public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)\r\n    {\r\n        var validateResult = await ValidateAsync(configuration);\r\n\r\n        if (validateResult.IsValid)\r\n        {\r\n            return new OkResponse<ConfigurationValidationResult>(new ConfigurationValidationResult(false));\r\n        }\r\n\r\n        var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage));\r\n\r\n        var result = new ConfigurationValidationResult(true, errors.Cast<Error>().ToList());\r\n\r\n        return new OkResponse<ConfigurationValidationResult>(result);\r\n    }\r\n\r\n    private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateRoute, List<FileRoute> routes)\r\n    {\r\n        var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key));\r\n\r\n        return routesForAggregate.Count() == fileAggregateRoute.RouteKeys.Count;\r\n    }\r\n\n    [GeneratedRegex(@\"\\{\\w+\\}\", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, \"en-US\")]\n    private static partial Regex PlaceholderRegex();\n\r\n    private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate)\r\n    {\r\n        var placeholders = PlaceholderRegex().Matches(pathTemplate)\n            .Select(m => m.Value).ToList();\r\n        return placeholders.Count == placeholders.Distinct().Count();\r\n    }\n\n    private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateRoute fileAggregateRoute,\r\n        IEnumerable<FileRoute> routes)\r\n    {\r\n        var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key));\r\n\r\n        return routesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey));\r\n    }\r\n\r\n    private static bool IsNotDuplicateIn(FileRoute route, IEnumerable<FileRoute> routes)\r\n    {\r\n        var matchingRoutes = routes\r\n            .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate\r\n                        && r.UpstreamHost == route.UpstreamHost\n                        && AreTheSame(r.UpstreamHeaderTemplates, route.UpstreamHeaderTemplates))\r\n            .ToArray();\r\n\r\n        if (matchingRoutes.Length == 1)\r\n        {\r\n            return true;\r\n        }\r\n\r\n        var allowAllVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count == 0);\r\n\r\n        var duplicateAllowAllVerbs = matchingRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1;\r\n\r\n        var specificVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count != 0);\r\n\r\n        var duplicateSpecificVerbs = matchingRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();\r\n\r\n        if (duplicateAllowAllVerbs || duplicateSpecificVerbs || allowAllVerbs && specificVerbs)\r\n        {\r\n            return false;\r\n        }\r\n\r\n        return true;\r\n    }\n\n    private static bool AreTheSame(IDictionary<string, string> upstreamHeaderTemplates, IDictionary<string, string> otherHeaderTemplates)\n        => upstreamHeaderTemplates.Count == otherHeaderTemplates.Count &&\n            upstreamHeaderTemplates.All(x => otherHeaderTemplates.ContainsKey(x.Key) && otherHeaderTemplates[x.Key] == x.Value);\r\n\r\n    private static bool IsNotDuplicateIn(FileRoute route,\r\n        IEnumerable<FileAggregateRoute> aggregateRoutes)\r\n    {\r\n        var duplicate = aggregateRoutes\r\n            .Any(a => a.UpstreamPathTemplate == route.UpstreamPathTemplate\r\n                        && a.UpstreamHost == route.UpstreamHost\r\n                        && route.UpstreamHttpMethod.Select(x => x.ToLower()).Contains(\"get\"));\r\n\r\n        return !duplicate;\r\n    }\r\n\r\n    private static bool IsNotDuplicateIn(FileAggregateRoute route, IEnumerable<FileAggregateRoute> aggregateRoutes)\r\n    {\r\n        var matchingRoutes = aggregateRoutes\n            .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate & r.UpstreamHost == route.UpstreamHost);\n        return matchingRoutes.Count() <= 1;\r\n    }\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs",
    "content": "using FluentValidation;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.Configuration.Validator;\n\npublic class FileGlobalConfigurationFluentValidator : AbstractValidator<FileGlobalConfiguration>\n{\n    public FileGlobalConfigurationFluentValidator(\n        FileQoSOptionsFluentValidator qosValidator,\n        FileAuthenticationOptionsValidator authValidator)\n    {\n        RuleFor(configuration => configuration.QoSOptions)\n            .SetValidator(qosValidator);\n\n        RuleFor(configuration => configuration.AuthenticationOptions)\n            .SetValidator(authValidator);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs",
    "content": "using FluentValidation;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.File;\nusing Ocelot.QualityOfService;\n\r\nnamespace Ocelot.Configuration.Validator;\r\n\r\npublic class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions>\r\n{\r\n    private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;\r\n\r\n    public FileQoSOptionsFluentValidator(IServiceProvider provider)\r\n    {\r\n        _qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();\r\n        When(UseQos, CheckRules);\r\n    }\r\n\r\n    private bool UseQos(FileQoSOptions opts) => new QoSOptions(opts).UseQos;\r\n    private void CheckRules()\r\n    {\r\n        RuleFor(qos => qos)\r\n            .Must(HaveQosHandlerRegistered)\r\n            .WithMessage($\"Unable to start Ocelot because either a {nameof(Route)} or {nameof(FileConfiguration.GlobalConfiguration)} are using {nameof(FileRoute.QoSOptions)} but no {nameof(QosDelegatingHandlerDelegate)} has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?\");\r\n    }\r\n\r\n    private bool HaveQosHandlerRegistered(FileQoSOptions arg)\r\n    {\r\n        return _qosDelegatingHandlerDelegate != null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/FileValidationFailedError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Configuration.Validator;\n\npublic class FileValidationFailedError : Error\n{\n    public FileValidationFailedError(string message)\n        : base(message, OcelotErrorCode.FileValidationFailedError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/HostAndPortValidator.cs",
    "content": "using FluentValidation;\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.Configuration.Validator;\r\n\r\npublic class HostAndPortValidator : AbstractValidator<FileHostAndPort>\r\n{\r\n    public HostAndPortValidator()\r\n    {\r\n        RuleFor(r => r.Host)\r\n            .NotEmpty()\r\n            .WithMessage(\"When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/IConfigurationValidator.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Configuration.Validator;\r\n\r\npublic interface IConfigurationValidator\r\n{\r\n    Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Configuration/Validator/RouteFluentValidator.cs",
    "content": "﻿using FluentValidation;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure;\n\r\nnamespace Ocelot.Configuration.Validator;\r\n\r\n/// <summary>\r\n/// Default implementation od the <see cref=\"AbstractValidator{FileRoute}\"/> abstract class.\r\n/// </summary>\r\npublic partial class RouteFluentValidator : AbstractValidator<FileRoute>\r\n{\r\n    public RouteFluentValidator(\r\n        HostAndPortValidator hostAndPortValidator,\r\n        FileQoSOptionsFluentValidator qosOptsValidator,\r\n        FileAuthenticationOptionsValidator authOptsValidator)\r\n    {\r\n        RuleFor(route => route.QoSOptions)\r\n            .SetValidator(qosOptsValidator);\r\n\r\n        RuleFor(route => route.DownstreamPathTemplate)\r\n            .NotEmpty()\r\n            .WithMessage(\"{PropertyName} cannot be empty\");\r\n\r\n        RuleFor(route => route.UpstreamPathTemplate)\r\n            .NotEmpty()\r\n            .WithMessage(\"{PropertyName} cannot be empty\");\r\n\r\n        When(route => !string.IsNullOrEmpty(route.DownstreamPathTemplate), () =>\r\n        {\r\n            RuleFor(route => route.DownstreamPathTemplate)\r\n                .Must(path => path.StartsWith('/'))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} doesnt start with forward slash\");\r\n\r\n            RuleFor(route => route.DownstreamPathTemplate)\r\n                .Must(path => !path.Contains(\"//\"))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n\r\n            RuleFor(route => route.DownstreamPathTemplate)\r\n                .Must(path => !path.Contains(\"https://\") && !path.Contains(\"http://\"))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} contains scheme\");\r\n        });\r\n\r\n        When(route => !string.IsNullOrEmpty(route.UpstreamPathTemplate), () =>\r\n        {\r\n            RuleFor(route => route.UpstreamPathTemplate)\r\n                .Must(path => !path.Contains(\"//\"))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n\r\n            RuleFor(route => route.UpstreamPathTemplate)\r\n                .Must(path => path.StartsWith('/'))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} doesnt start with forward slash\");\r\n\r\n            RuleFor(route => route.UpstreamPathTemplate)\r\n                .Must(path => !path.Contains(\"https://\") && !path.Contains(\"http://\"))\r\n                .WithMessage(\"{PropertyName} {PropertyValue} contains scheme\");\r\n        });\r\n\r\n        When(route => route.RateLimitOptions != null && route.RateLimitOptions.EnableRateLimiting != false, () =>\r\n        {\r\n            RuleFor(route => route.RateLimitOptions.Limit)\r\n                .Must(limit => !limit.HasValue || (limit.HasValue && limit.Value > 0))\r\n                .WithMessage(route => $\"RateLimitOptions.Limit is negative or zero for the route {route}\");\r\n            RuleFor(route => route.RateLimitOptions.Period)\r\n                .NotEmpty()\r\n                .WithMessage(\"RateLimitOptions.Period is empty\");\r\n\r\n            RuleFor(route => route.RateLimitOptions)\r\n                .Must(IsValidPeriod)\r\n                .WithMessage(\"RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period\");\r\n        });\r\n\r\n        RuleFor(route => route.AuthenticationOptions)\r\n            .SetValidator(authOptsValidator);\r\n\r\n        When(route => string.IsNullOrEmpty(route.ServiceName), () =>\r\n        {\r\n            RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()\r\n                .WithMessage(\"When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!\");\r\n        });\r\n\r\n        When(route => string.IsNullOrEmpty(route.ServiceName), () =>\r\n        {\r\n            RuleForEach(route => route.DownstreamHostAndPorts)\r\n                .SetValidator(hostAndPortValidator);\r\n        });\r\n\r\n        When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersion), () =>\r\n        {\r\n            RuleFor(r => r.DownstreamHttpVersion).Matches(\"^[0-9]([.,][0-9]{1,1})?$\");\r\n        });\r\n\n        When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () =>\n        {\n            RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@\"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$\");\n        });\n    }\r\n\r\n    [GeneratedRegex(@\"^\\d+(\\.\\d+)?ms\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex MilliSecondsRegex();\r\n\r\n    [GeneratedRegex(@\"^\\d+(\\.\\d+)?s\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex SecondsRegex();\r\n\r\n    [GeneratedRegex(@\"^\\d+(\\.\\d+)?m\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex MinutesRegex();\r\n\r\n    [GeneratedRegex(@\"^\\d+(\\.\\d+)?h\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex HoursRegex();\r\n\r\n    [GeneratedRegex(@\"^\\d+(\\.\\d+)?d\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\r\n    private static partial Regex DaysRegex();\r\n\r\n    private static bool IsValidPeriod(FileRateLimitByHeaderRule rateLimitOptions)\r\n    {\r\n        if (string.IsNullOrEmpty(rateLimitOptions.Period))\r\n        {\r\n            return false;\r\n        }\r\n\r\n        var period = rateLimitOptions.Period.Trim();\r\n        return MilliSecondsRegex().Match(period).Success\r\n               || SecondsRegex().Match(period).Success\r\n               || MinutesRegex().Match(period).Success\r\n               || HoursRegex().Match(period).Success\r\n               || DaysRegex().Match(period).Success;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs",
    "content": "using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Configuration.Memory;\nusing Newtonsoft.Json;\r\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure;\n\r\nnamespace Ocelot.DependencyInjection;\r\n\r\n/// <summary>\n/// Defines extension-methods for the <see cref=\"IConfigurationBuilder\"/> interface.\n/// </summary>\npublic static partial class ConfigurationBuilderExtensions\r\n{\r\n    public const string PrimaryConfigFile = \"ocelot.json\";\n    public const string GlobalConfigFile = \"ocelot.global.json\";\n    public const string EnvironmentConfigFile = \"ocelot.{0}.json\";\n\n    [Obsolete(\"Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl\")]\r\n    public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl)\r\n    {\r\n        var memorySource = new MemoryConfigurationSource\r\n        {\r\n            InitialData = new List<KeyValuePair<string, string>>\r\n            {\r\n                new(\"BaseUrl\", baseUrl),\r\n            },\r\n        };\r\n\r\n        return builder.Add(memorySource);\r\n    }\r\n\r\n    /// <summary>\n    /// Adds Ocelot configuration by environment, reading the required files from the default path.\n    /// </summary>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"env\">Web hosting environment object.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env)\n        => builder.AddOcelot(\".\", env);\r\n\r\n    /// <summary>\n    /// Adds Ocelot configuration by environment, reading the required files from the specified folder.\n    /// </summary>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"folder\">Folder to read files from.</param>\n    /// <param name=\"env\">Web hosting environment object.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\r\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env)\n        => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile);\n\n    /// <summary>\n    /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder.\n    /// </summary>\n    /// <remarks>Use optional arguments for injections and overridings.</remarks>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"env\">Web hosting environment object.</param>\n    /// <param name=\"mergeTo\">Option to merge files to.</param>\n    /// <param name=\"primaryConfigFile\">Primary config file.</param>\r\n    /// <param name=\"globalConfigFile\">Global config file.</param>\r\n    /// <param name=\"environmentConfigFile\">Environment config file.</param>\r\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\r\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\n        => builder.AddOcelot(\".\", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange);\n\n    /// <summary>\n    /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder.\n    /// </summary>\n    /// <remarks>Use optional arguments for injections and overridings.</remarks>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"folder\">Folder to read files from.</param>\n    /// <param name=\"env\">Web hosting environment object.</param>\n    /// <param name=\"mergeTo\">Option to merge files to.</param>\n    /// <param name=\"primaryConfigFile\">Primary config file.</param>\r\n    /// <param name=\"globalConfigFile\">Global config file.</param>\r\n    /// <param name=\"environmentConfigFile\">Environment config file.</param>\r\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\r\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\r\n    {\r\n        var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile);\r\n        primaryConfigFile ??= Path.Join(folder, PrimaryConfigFile); // if not specified, merge & write back to the same folder\r\n        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);\r\n    }\n\n    private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json,\n        string primaryConfigFile, bool? optional, bool? reloadOnChange)\n    {\n        return mergeTo == MergeOcelotJson.ToMemory ? \n            builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : \n            AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);\n    }\r\n\r\n    [GeneratedRegex(@\"^ocelot\\.(.*?)\\.json$\", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, \"en-US\")]\n    private static partial Regex SubConfigRegex();\n\r\n    private static string GetMergedOcelotJson(string folder, IWebHostEnvironment env,\n        FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null)\r\n    {\r\n        // All versions of overloaded AddOcelot methods call this GetMergedOcelotJson one, so we improve Regex performance by cache increasing.\r\n        // Developers can adjust the RegexGlobal value BEFORE calling AddOcelot\r\n        // Developers can adjust the Regex.CacheSize value AFTER calling AddOcelot\r\n        Regex.CacheSize = RegexGlobal.RegexCacheSize;\n\r\n        var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? \"Development\" : env.EnvironmentName;\r\n        environmentFile ??= Path.Join(folder, string.Format(EnvironmentConfigFile, envName));\n        var reg = SubConfigRegex();\n        var environmentFileInfo = new FileInfo(environmentFile);\r\n        var files = new DirectoryInfo(folder)\r\n            .EnumerateFiles()\r\n            .Where(fi => reg.IsMatch(fi.Name) &&\n                !fi.Name.Equals(environmentFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&\n                !fi.FullName.Equals(environmentFileInfo.FullName, StringComparison.OrdinalIgnoreCase))\r\n            .ToArray();\r\n\n        fileConfiguration ??= new FileConfiguration();\n        primaryFile ??= Path.Join(folder, PrimaryConfigFile);\n        globalFile ??= Path.Join(folder, GlobalConfigFile);\r\n        var primaryFileInfo = new FileInfo(primaryFile);\n        var globalFileInfo = new FileInfo(globalFile);\n        foreach (var file in files)\r\n        {\r\n            if (files.Length > 1 &&\n                file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&\n                file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase))\r\n            {\r\n                continue;\r\n            }\r\n\r\n            var lines = File.ReadAllText(file.FullName);\r\n            var config = JsonConvert.DeserializeObject<FileConfiguration>(lines);\r\n            if (file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&\n                file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase))\r\n            {\r\n                fileConfiguration.GlobalConfiguration = config.GlobalConfiguration;\r\n            }\r\n\r\n            fileConfiguration.Aggregates.AddRange(config.Aggregates);\r\n            fileConfiguration.Routes.AddRange(config.Routes);\r\n        }\r\n\r\n        return JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);\r\n    }\n\n    /// <summary>\n    /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.<br/>\n    /// Finally, adds JSON file as configuration provider.\n    /// </summary>\n    /// <remarks>Use optional arguments for injections and overridings.</remarks>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"fileConfiguration\">File configuration to add as JSON provider.</param>\r\n    /// <param name=\"primaryConfigFile\">Primary config file.</param>\r\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\r\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration,\n        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\r\n    {\r\n        var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);\n        return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);\n    }\n\n    /// <summary>\n    /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder.\n    /// </summary>\n    /// <param name=\"builder\">Configuration builder to extend.</param>\n    /// <param name=\"fileConfiguration\">File configuration to add as JSON provider.</param>\n    /// <param name=\"env\">Web hosting environment object.</param>\n    /// <param name=\"mergeTo\">Option to merge files to.</param>\n    /// <param name=\"primaryConfigFile\">Primary config file.</param>\r\n    /// <param name=\"globalConfigFile\">Global config file.</param>\r\n    /// <param name=\"environmentConfigFile\">Environment config file.</param>\r\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\r\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo,\n        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\r\n    {\r\n        var json = GetMergedOcelotJson(\".\", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile);\r\n        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);\r\n    }\n\n    /// <summary>\n    /// Adds Ocelot primary configuration file (aka ocelot.json).<br/>\n    /// Writes JSON to the file.<br/>\n    /// Adds the file as a JSON configuration provider via the <see cref=\"JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)\"/> extension.\n    /// </summary>\n    /// <remarks>Use optional arguments for injections and overridings.</remarks>\n    /// <param name=\"builder\">The builder to extend.</param>\n    /// <param name=\"json\">JSON data of the Ocelot configuration.</param>\n    /// <param name=\"primaryFile\">Primary config file.</param>\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    private static IConfigurationBuilder AddOcelotJsonFile(IConfigurationBuilder builder, string json,\n        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\n    {\n        var primary = primaryFile ?? PrimaryConfigFile;\n        File.WriteAllText(primary, json);\r\n        return builder?.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false);\n    }\r\n\r\n    /// <summary>\n    /// Adds Ocelot primary configuration file (aka ocelot.json) in read-only mode.\r\n    /// <para>Adds the file as a JSON configuration provider via the <see cref=\"JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)\"/> extension.</para>\n    /// </summary>\n    /// <remarks>Use optional arguments for injections and overridings.</remarks>\n    /// <param name=\"builder\">The builder to extend.</param>\n    /// <param name=\"primaryFile\">Primary config file path.</param>\n    /// <param name=\"optional\">The 2nd argument of the AddJsonFile.</param>\n    /// <param name=\"reloadOnChange\">The 3rd argument of the AddJsonFile.</param>\n    /// <returns>An <see cref=\"IConfigurationBuilder\"/> object.</returns>\n    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder,\r\n        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections\r\n        => builder.AddJsonFile(primaryFile ?? PrimaryConfigFile, optional ?? false, reloadOnChange ?? false);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/Features.cs",
    "content": "﻿using FluentValidation;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Cache;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\nusing Ocelot.Logging;\nusing Ocelot.RateLimiting;\n\nnamespace Ocelot.DependencyInjection;\n\npublic static class Features\n{\n    /// <summary>This Ocelot Core feature adds validation for JSON configuration File-models.</summary>\n    /// <remarks>Added validator-classes must implement the <see cref=\"AbstractValidator{FileConfiguration}\"/> interface, where T is File-model.</remarks>\n    /// <param name=\"services\">The services collection to add the feature to.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> object.</returns>\n    public static IServiceCollection AddConfigurationValidators(this IServiceCollection services) => services\n        .AddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>()\n        .AddSingleton<HostAndPortValidator>()\n        .AddSingleton<RouteFluentValidator>()\n        .AddSingleton<FileGlobalConfigurationFluentValidator>()\n        .AddSingleton<FileQoSOptionsFluentValidator>()\n        .AddSingleton<FileAuthenticationOptionsValidator>();\n\n    /// <summary>\n    /// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/ratelimiting.rst\">Rate Limiting</see>.\n    /// </summary>\n    /// <remarks>\n    /// Read The Docs: <see href=\"https://ocelot.readthedocs.io/en/latest/features/ratelimiting.html\">Rate Limiting</see>.\n    /// </remarks>\n    /// <param name=\"services\">The services collection to add the feature to.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> object.</returns>\n    public static IServiceCollection AddOcelotRateLimiting(this IServiceCollection services) => services\n        .AddSingleton<IRateLimiting, RateLimiting.RateLimiting>()\n        .AddSingleton<IRateLimitStorage, MemoryCacheRateLimitStorage>();\n\n    /// <summary>\n    /// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/caching.rst\">Request Caching</see>.\n    /// </summary>\n    /// <remarks>\n    /// Read The Docs: <see href=\"https://ocelot.readthedocs.io/en/latest/features/caching.html\">Caching</see>.\n    /// </remarks>\n    /// <param name=\"services\">The services collection to add the feature to.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> object.</returns>\n    public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services\n        .AddSingleton<IOcelotCache<Regex>, DefaultMemoryCache<Regex>>()\n        .AddSingleton<IOcelotCache<FileConfiguration>, DefaultMemoryCache<FileConfiguration>>()\n        .AddSingleton<IOcelotCache<CachedResponse>, DefaultMemoryCache<CachedResponse>>()\n        .AddSingleton<ICacheKeyGenerator, DefaultCacheKeyGenerator>()\n        .AddSingleton<ICacheOptionsCreator, CacheOptionsCreator>()\n        .AddMemoryCache();\n\n    /// <summary>\n    /// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers\">Routing based on request header</see>.\n    /// </summary>\n    /// <param name=\"services\">The services collection to add the feature to.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> object.</returns>\n    public static IServiceCollection AddOcelotHeaderRouting(this IServiceCollection services) => services\n        .AddSingleton<IUpstreamHeaderTemplatePatternCreator, UpstreamHeaderTemplatePatternCreator>()\n        .AddSingleton<IHeadersToHeaderTemplatesMatcher, HeadersToHeaderTemplatesMatcher>()\n        .AddSingleton<IHeaderPlaceholderNameAndValueFinder, HeaderPlaceholderNameAndValueFinder>();\n\n    public static IServiceCollection AddOcelotLogging(this IServiceCollection services) => services\n        .AddSingleton<IOcelotLoggerFactory, OcelotLoggerFactory>()\n        .AddSingleton<OcelotDiagnosticListener>()\n        .AddLogging();\n\n    /// <summary>\n    /// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/metadata.rst\">Inject custom metadata and use it in delegating handlers</see>.\n    /// </summary>\n    /// <param name=\"services\">The services collection to add the feature to.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> object.</returns>\n    public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => \n        services.AddSingleton<IMetadataCreator, DefaultMetadataCreator>();\n}\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/IOcelotBuilder.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Multiplexer;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.DependencyInjection;\n\npublic interface IOcelotBuilder\n{\n    IServiceCollection Services { get; }\n\n    IConfiguration Configuration { get; }\n\n    IMvcCoreBuilder MvcCoreBuilder { get; }\n\n    /// <summary>\n    /// Adds a <see cref=\"DelegatingHandler\"/> of the <paramref name=\"delegateType\"/> type as a transient service, with the <paramref name=\"global\"/> option to make the handler globally available.\n    /// </summary>\n    /// <param name=\"delegateType\">The type of a <see cref=\"DelegatingHandler\"/> to be registered.</param>\n    /// <param name=\"global\">True if the handler should be globally available.</param>\n    /// <returns>The reference to the same <see cref=\"IOcelotBuilder\"/> object.</returns>\n    /// <exception cref=\"ArgumentOutOfRangeException\">Generates an exception if the <paramref name=\"delegateType\"/> type does not inherit from the <see cref=\"DelegatingHandler\"/>.</exception>\n    IOcelotBuilder AddDelegatingHandler(Type delegateType, bool global = false);\n\n    /// <summary>\n    /// Adds a <see cref=\"DelegatingHandler\"/> of the <typeparamref name=\"THandler\"/> type as a transient service, with the <paramref name=\"global\"/> option to make the handler globally available.\n    /// </summary>\n    /// <typeparam name=\"THandler\">The type of a <see cref=\"DelegatingHandler\"/> to be registered.</typeparam>\n    /// <param name=\"global\">True if the handler should be globally available.</param>\n    /// <returns>The reference to the same <see cref=\"IOcelotBuilder\"/> object.</returns>\n    IOcelotBuilder AddDelegatingHandler<THandler>(bool global = false)\n        where THandler : DelegatingHandler;\n\n    IOcelotBuilder AddSingletonDefinedAggregator<T>()\n        where T : class, IDefinedAggregator;\n\n    IOcelotBuilder AddTransientDefinedAggregator<T>()\n        where T : class, IDefinedAggregator;\n\n    IOcelotBuilder AddCustomLoadBalancer<T>()\n        where T : ILoadBalancer, new();\n\n    IOcelotBuilder AddCustomLoadBalancer<T>(Func<T> loadBalancerFactoryFunc)\n        where T : ILoadBalancer;\n\n    IOcelotBuilder AddCustomLoadBalancer<T>(Func<IServiceProvider, T> loadBalancerFactoryFunc)\n        where T : ILoadBalancer;\n\n    IOcelotBuilder AddCustomLoadBalancer<T>(\n        Func<DownstreamRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)\n        where T : ILoadBalancer;\n\n    IOcelotBuilder AddCustomLoadBalancer<T>(\n        Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)\n        where T : ILoadBalancer;\n\n    IOcelotBuilder AddConfigPlaceholders();\n}\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/MergeOcelotJson.cs",
    "content": "﻿namespace Ocelot.DependencyInjection;\n\npublic enum MergeOcelotJson\n{\n    /// <summary>\n    /// The option to merge all configuration files to one primary config file aka ocelot.json.\n    /// </summary>\n    ToFile = 0,\n\n    /// <summary>\n    /// The option to merge all configuration files to memory and reuse the config by in-memory configuration provider.\n    /// </summary>\n    ToMemory = 1,\n}\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/OcelotBuilder.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Administration;\nusing Ocelot.Authorization;\nusing Ocelot.Claims;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Parser;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Configuration.Setter;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.DownstreamUrlCreator;\nusing Ocelot.Headers;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Multiplexer;\nusing Ocelot.PathManipulation;\nusing Ocelot.QualityOfService;\nusing Ocelot.QueryStrings;\nusing Ocelot.Request.Creator;\nusing Ocelot.Request.Mapper;\nusing Ocelot.Requester;\nusing Ocelot.Responder;\nusing Ocelot.Security;\nusing Ocelot.Security.IPSecurity;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.WebSockets;\nusing System.Reflection;\n\nnamespace Ocelot.DependencyInjection;\n\npublic class OcelotBuilder : IOcelotBuilder\n{\n    public IServiceCollection Services { get; }\n    public IConfiguration Configuration { get; }\n    public IMvcCoreBuilder MvcCoreBuilder { get; }\n\n    public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder = null)\n    {\n        Configuration = configurationRoot;\n        Services = services;\n        Services.Configure<FileConfiguration>(configurationRoot);\n        Services.Configure<FileGlobalConfiguration>(configurationRoot.GetSection(nameof(FileConfiguration.GlobalConfiguration)));\n        Services.AddConfigurationValidators(); // based on the AbstractValidator<FileModel> interface\n\n        Services.TryAddSingleton<IHttpResponseHeaderReplacer, HttpResponseHeaderReplacer>();\n        Services.TryAddSingleton<IHttpContextRequestHeaderReplacer, HttpContextRequestHeaderReplacer>();\n        Services.TryAddSingleton<IHeaderFindAndReplaceCreator, HeaderFindAndReplaceCreator>();\n        Services.TryAddSingleton<IInternalConfigurationCreator, FileInternalConfigurationCreator>();\n        Services.TryAddSingleton<IInternalConfigurationRepository, InMemoryInternalConfigurationRepository>();\n        Services.TryAddSingleton<IRoutesCreator, StaticRoutesCreator>();\n        Services.TryAddSingleton<IDynamicsCreator, DynamicRoutesCreator>();\n        Services.TryAddSingleton<IAggregatesCreator, AggregatesCreator>();\n        Services.TryAddSingleton<IRouteKeyCreator, RouteKeyCreator>();\n        Services.TryAddSingleton<IConfigurationCreator, ConfigurationCreator>();\n        Services.TryAddSingleton<ILoadBalancerOptionsCreator, LoadBalancerOptionsCreator>();\n        Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();\n        Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();\n        Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();\n        Services.TryAddSingleton<IRequestIdKeyCreator, RequestIdKeyCreator>();\n        Services.TryAddSingleton<IServiceProviderConfigurationCreator, ServiceProviderConfigurationCreator>();\n        Services.TryAddSingleton<IQoSOptionsCreator, QoSOptionsCreator>();\n        Services.TryAddSingleton<IRateLimitOptionsCreator, RateLimitOptionsCreator>();\n        Services.TryAddSingleton<IBaseUrlFinder, BaseUrlFinder>();\n        Services.TryAddSingleton<IFileConfigurationRepository, DiskFileConfigurationRepository>();\n        Services.TryAddSingleton<IFileConfigurationSetter, FileAndInternalConfigurationSetter>();\n        Services.TryAddSingleton<IServiceDiscoveryProviderFactory, ServiceDiscoveryProviderFactory>();\n        Services.AddSingleton<ILoadBalancerCreator, NoLoadBalancerCreator>();\n        Services.AddSingleton<ILoadBalancerCreator, RoundRobinCreator>();\n        Services.AddSingleton<ILoadBalancerCreator, CookieStickySessionsCreator>();\n        Services.AddSingleton<ILoadBalancerCreator, LeastConnectionCreator>();\n        Services.TryAddSingleton<ILoadBalancerFactory, LoadBalancerFactory>();\n        Services.TryAddSingleton<ILoadBalancerHouse, LoadBalancerHouse>();\n        Services.TryAddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();\n        Services.TryAddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();\n        Services.TryAddSingleton<IClaimsAuthorizer, ClaimsAuthorizer>();\n        Services.TryAddSingleton<IScopesAuthorizer, ScopesAuthorizer>();\n        Services.TryAddSingleton<IAddClaimsToRequest, AddClaimsToRequest>();\n        Services.TryAddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();\n        Services.TryAddSingleton<IAddQueriesToRequest, AddQueriesToRequest>();\n        Services.TryAddSingleton<IChangeDownstreamPathTemplate, ChangeDownstreamPathTemplate>();\n        Services.TryAddSingleton<IClaimsParser, ClaimsParser>();\n        Services.TryAddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();\n        Services.TryAddSingleton<IPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();\n        Services.TryAddSingleton<IDownstreamPathPlaceholderReplacer, DownstreamPathPlaceholderReplacer>();\n        Services.AddSingleton<IDownstreamRouteProvider, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();\n        Services.AddSingleton<IDownstreamRouteProvider, DiscoveryDownstreamRouteFinder>();\n        Services.TryAddSingleton<IDownstreamRouteProviderFactory, DownstreamRouteProviderFactory>();\n        Services.TryAddSingleton<IHttpResponder, HttpContextResponder>();\n        Services.TryAddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();\n        Services.TryAddSingleton<IRequestMapper, RequestMapper>();\n        Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>();\n        Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();\n        Services.TryAddSingleton<IDelegatingHandlerFactory, DelegatingHandlerFactory>();\n        \n        Services.TryAddSingleton<IOcelotConfigurationChangeTokenSource, OcelotConfigurationChangeTokenSource>();\n        Services.TryAddSingleton<IOptionsMonitor<IInternalConfiguration>, OcelotConfigurationMonitor>();\n\n        // Chinese developers should read StackOverflow ignoring Microsoft Learn docs -> http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc\n        Services.AddHttpContextAccessor();\n        Services.TryAddSingleton<IRequestScopedDataRepository, HttpDataRepository>();\n        Services.TryAddSingleton<IResponseAggregator, SimpleJsonResponseAggregator>();\n        Services.TryAddSingleton<ITracingHandlerFactory, TracingHandlerFactory>();\n        Services.TryAddSingleton<IFileConfigurationPollerOptions, InMemoryFileConfigurationPollerOptions>();\n        Services.TryAddSingleton<IAddHeadersToResponse, AddHeadersToResponse>();\n        Services.TryAddSingleton<IPlaceholders, Placeholders>();\n        Services.TryAddSingleton<IResponseAggregatorFactory, InMemoryResponseAggregatorFactory>();\n        Services.TryAddSingleton<IDefinedAggregatorProvider, ServiceLocatorDefinedAggregatorProvider>();\n        Services.TryAddSingleton<IDownstreamRequestCreator, DownstreamRequestCreator>();\n        Services.TryAddSingleton<IFrameworkDescription, FrameworkDescription>();\n        Services.TryAddSingleton<IQoSFactory, QoSFactory>();\n        Services.TryAddSingleton<IExceptionToErrorMapper, HttpExceptionToErrorMapper>();\n        Services.TryAddSingleton<IVersionCreator, HttpVersionCreator>();\n        Services.TryAddSingleton<IVersionPolicyCreator, HttpVersionPolicyCreator>();\n        Services.TryAddSingleton<IWebSocketsFactory, WebSocketsFactory>();\n\n        // Add security\n        Services.TryAddSingleton<ISecurityOptionsCreator, SecurityOptionsCreator>();\n        Services.TryAddSingleton<ISecurityPolicy, IPSecurityPolicy>();\n\n        // Features\n        Services.AddOcelotCache();\n        Services.AddOcelotHeaderRouting();\n        Services.AddOcelotLogging();\n        Services.AddOcelotMessageInvokerPool();\n        Services.AddOcelotMetadata();\n        Services.AddOcelotRateLimiting();\n\n        // Add ASP.NET services\n        var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly;\n        MvcCoreBuilder = (customBuilder ?? AddDefaultAspNetServices)\n            .Invoke(Services.AddMvcCore(), assembly);\n    }\n\n    /// <summary>\n    /// Adds default ASP.NET services which are the minimal part of the gateway core.\n    /// <para>\n    /// Finally the builder adds Newtonsoft.Json services via the <see cref=\"NewtonsoftJsonMvcCoreBuilderExtensions.AddNewtonsoftJson(IMvcCoreBuilder)\"/> extension-method.<br/>\n    /// To remove these services, use custom builder in the <see cref=\"ServiceCollectionExtensions.AddOcelotUsingBuilder(IServiceCollection, Func{IMvcCoreBuilder, Assembly, IMvcCoreBuilder})\"/> extension-method.\n    /// </para>\n    /// </summary>\n    /// <remarks>\n    /// Note that the following <see cref=\"IServiceCollection\"/> extensions being called:<br/>\n    /// - <see cref=\"MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)\"/>, impossible to remove.<br/>\n    /// - <see cref=\"AnalysisServiceCollectionExtensions.AddMiddlewareAnalysis(IServiceCollection)\"/><br/>\n    /// - <see cref=\"EncoderServiceCollectionExtensions.AddWebEncoders(IServiceCollection)\"/>.\n    /// <para>\n    /// Warning! The following <see cref=\"IMvcCoreBuilder\"/> extensions being called:<br/>\n    /// - <see cref=\"MvcCoreMvcCoreBuilderExtensions.AddApplicationPart(IMvcCoreBuilder, Assembly)\"/><br/>\n    /// - <see cref=\"MvcCoreMvcCoreBuilderExtensions.AddControllersAsServices(IMvcCoreBuilder)\"/><br/>\n    /// - <see cref=\"MvcCoreMvcCoreBuilderExtensions.AddAuthorization(IMvcCoreBuilder)\"/><br/>\n    /// - <see cref=\"NewtonsoftJsonMvcCoreBuilderExtensions.AddNewtonsoftJson(IMvcCoreBuilder)\"/>, removable.\n    /// </para>\n    /// </remarks>\n    /// <param name=\"builder\">The default builder being returned by <see cref=\"MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)\"/> extension-method.</param>\n    /// <param name=\"assembly\">The web app assembly.</param>\n    /// <returns>An <see cref=\"IMvcCoreBuilder\"/> object.</returns>\n    protected IMvcCoreBuilder AddDefaultAspNetServices(IMvcCoreBuilder builder, Assembly assembly)\n    {\n        Services\n            .AddMiddlewareAnalysis()\n            .AddWebEncoders();\n\n        return builder\n            .AddApplicationPart(assembly)\n            .AddControllersAsServices()\n            .AddAuthorization()\n            .AddNewtonsoftJson();\n    }\n\n    public IOcelotBuilder AddSingletonDefinedAggregator<T>()\n        where T : class, IDefinedAggregator\n    {\n        Services.AddSingleton<IDefinedAggregator, T>();\n        return this;\n    }\n\n    public IOcelotBuilder AddTransientDefinedAggregator<T>()\n        where T : class, IDefinedAggregator\n    {\n        Services.AddTransient<IDefinedAggregator, T>();\n        return this;\n    }\n\n    public IOcelotBuilder AddCustomLoadBalancer<TLoadBalancer>()\n        where TLoadBalancer : ILoadBalancer, new()\n    {\n        static TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider)\n            => new(); // TODO Not tested by acceptance tests, Assert another constructors with injected params?\n        return AddCustomLoadBalancer<TLoadBalancer>(Create);\n    }\n\n    public IOcelotBuilder AddCustomLoadBalancer<TLoadBalancer>(Func<TLoadBalancer> loadBalancerFactoryFunc)\n        where TLoadBalancer : ILoadBalancer\n    {\n        TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider)\n            => loadBalancerFactoryFunc();\n        return AddCustomLoadBalancer<TLoadBalancer>(Create);\n    }\n\n    public IOcelotBuilder AddCustomLoadBalancer<TLoadBalancer>(Func<IServiceProvider, TLoadBalancer> loadBalancerFactoryFunc)\n        where TLoadBalancer : ILoadBalancer\n    {\n        TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider)\n            => loadBalancerFactoryFunc(provider);\n        return AddCustomLoadBalancer<TLoadBalancer>(Create);\n    }\n\n    public IOcelotBuilder AddCustomLoadBalancer<TLoadBalancer>(Func<DownstreamRoute, IServiceDiscoveryProvider, TLoadBalancer> loadBalancerFactoryFunc)\n        where TLoadBalancer : ILoadBalancer\n    {\n        TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider)\n            => loadBalancerFactoryFunc(route, discoveryProvider);\n        return AddCustomLoadBalancer<TLoadBalancer>(Create);\n    }\n\n    public IOcelotBuilder AddCustomLoadBalancer<TLoadBalancer>(Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, TLoadBalancer> loadBalancerFactoryFunc)\n        where TLoadBalancer : ILoadBalancer\n    {\n        ILoadBalancer Create(DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider)\n            => loadBalancerFactoryFunc(_serviceProvider, route, discoveryProvider);\n        ILoadBalancerCreator implementationFactory(IServiceProvider provider)\n        {\n            _serviceProvider = provider;\n            return new DelegateInvokingLoadBalancerCreator<TLoadBalancer>(Create);\n        }\n\n        Services.AddSingleton<ILoadBalancerCreator>(implementationFactory);\n        return this;\n    }\n\n    /// <summary>\n    /// Adds a <see cref=\"DelegatingHandler\"/> of the <paramref name=\"delegateType\"/> type as a transient service, with the <paramref name=\"global\"/> option to make the handler globally available.\n    /// </summary>\n    /// <param name=\"delegateType\">The type of a <see cref=\"DelegatingHandler\"/> to be registered.</param>\n    /// <param name=\"global\">True if the handler should be globally available.</param>\n    /// <returns>The reference to the same <see cref=\"IOcelotBuilder\"/> object.</returns>\n    /// <exception cref=\"ArgumentOutOfRangeException\">Generates an exception if the <paramref name=\"delegateType\"/> type does not inherit from the <see cref=\"DelegatingHandler\"/>.</exception>\n    public IOcelotBuilder AddDelegatingHandler(Type delegateType, bool global = false)\n    {\n        if (!typeof(DelegatingHandler).IsAssignableFrom(delegateType))\n        {\n            throw new ArgumentOutOfRangeException(nameof(delegateType), delegateType.Name, \"It is not a delegating handler\");\n        }\n\n        if (global)\n        {\n            Services.AddTransient(delegateType);\n            Services.AddTransient(provider =>\n            {\n                var service = provider.GetService(delegateType) as DelegatingHandler;\n                return new GlobalDelegatingHandler(service);\n            });\n        }\n        else\n        {\n            Services.AddTransient(typeof(DelegatingHandler), delegateType);\n        }\n\n        return this;\n    }\n\n    /// <summary>\n    /// Adds a <see cref=\"DelegatingHandler\"/> of the <typeparamref name=\"THandler\"/> type as a transient service, with the <paramref name=\"global\"/> option to make the handler globally available.\n    /// </summary>\n    /// <typeparam name=\"THandler\">The type of a <see cref=\"DelegatingHandler\"/> to be registered.</typeparam>\n    /// <param name=\"global\">True if the handler should be globally available.</param>\n    /// <returns>The reference to the same <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public IOcelotBuilder AddDelegatingHandler<THandler>(bool global = false)\n        where THandler : DelegatingHandler\n    {\n        if (global)\n        {\n            Services.AddTransient<THandler>();\n            Services.AddTransient(provider =>\n            {\n                var service = provider.GetService<THandler>();\n                return new GlobalDelegatingHandler(service);\n            });\n        }\n        else\n        {\n            Services.AddTransient<DelegatingHandler, THandler>();\n        }\n\n        return this;\n    }\n\n    public IOcelotBuilder AddConfigPlaceholders()\n    {\n        // see: https://greatrexpectations.com/2018/10/25/decorators-in-net-core-with-dependency-injection\n        var wrappedDescriptor = Services.First(x => x.ServiceType == typeof(IPlaceholders));\n\n        var objectFactory = ActivatorUtilities.CreateFactory(\n            typeof(ConfigAwarePlaceholders),\n            new[] { typeof(IPlaceholders) });\n\n        Services.Replace(ServiceDescriptor.Describe(\n            typeof(IPlaceholders),\n            provider => (IPlaceholders)objectFactory(\n                provider,\n                new[] { CreateInstance(provider, wrappedDescriptor) }),\n            wrappedDescriptor.Lifetime\n        ));\n\n        return this;\n    }\n\n    /// <summary>For local implementation purposes, so it MUST NOT be public!..</summary>\n    private IServiceProvider _serviceProvider; // TODO Reuse ActivatorUtilities factories?\n\n    private static object CreateInstance(IServiceProvider provider, ServiceDescriptor descriptor)\n    {\n        if (descriptor.ImplementationInstance != null)\n        {\n            return descriptor.ImplementationInstance;\n        }\n\n        if (descriptor.ImplementationFactory != null)\n        {\n            return descriptor.ImplementationFactory(provider);\n        }\n\n        return ActivatorUtilities.GetServiceOrCreateInstance(provider, descriptor.ImplementationType);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing System.Reflection;\n\nnamespace Ocelot.DependencyInjection;\n\npublic static class ServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds default ASP.NET services and Ocelot application services.<br/>\n    /// Creates default <see cref=\"IConfiguration\"/> from the current service descriptors.\n    /// If the configuration is not registered, it will try to read ocelot configuration from current working directory.\n    /// </summary>\n    /// <remarks>\n    /// Remarks for default ASP.NET services being injected see in docs of the <see cref=\"OcelotBuilder.AddDefaultAspNetServices(IMvcCoreBuilder, Assembly)\"/> method.\n    /// </remarks>\n    /// <param name=\"services\">Current services collection.</param>\n    /// <returns>An <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddOcelot(this IServiceCollection services)\n    {\n        var configuration = services.FindConfiguration(null);\n        return new OcelotBuilder(services, configuration);\n    }\n\n    /// <summary>\n    /// Adds default ASP.NET services and Ocelot application services with configuration.\n    /// </summary>\n    /// <remarks>\n    /// Remarks for default ASP.NET services will be injected, see docs of the <see cref=\"OcelotBuilder.AddDefaultAspNetServices(IMvcCoreBuilder, Assembly)\"/> method.\n    /// </remarks>\n    /// <param name=\"services\">Current services collection.</param>\n    /// <param name=\"configuration\">Current web app configuration.</param>\n    /// <returns>An <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration)\n    {\n        return new OcelotBuilder(services, configuration);\n    }\n\n    /// <summary>\n    /// Adds Ocelot application services and custom ASP.NET services with custom builder.<br/>\n    /// Creates default <see cref=\"IConfiguration\"/> from the current service descriptors.\n    /// If the configuration is not registered, it will try to read ocelot configuration from current working directory.\n    /// </summary>\n    /// <remarks>\n    /// Warning! To understand which ASP.NET services should be injected/removed by custom builder, see docs of the <see cref=\"OcelotBuilder.AddDefaultAspNetServices(IMvcCoreBuilder, Assembly)\"/> method.\n    /// </remarks>\n    /// <param name=\"services\">Current services collection.</param>\n    /// <param name=\"customBuilder\">Current custom builder for ASP.NET MVC pipeline.</param>\n    /// <returns>An <see cref=\"IOcelotBuilder\"/> object.</returns>\n    [Obsolete(\"Use AddOcelotUsingBuilder() overloaded version with the 'IConfiguration configuration' parameter.\")]\n    public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder)\n    {\n        var configuration = services.FindConfiguration(null);\n        return new OcelotBuilder(services, configuration, customBuilder);\n    }\n\n    /// <summary>\n    /// Adds Ocelot application services and custom ASP.NET services with configuration and custom builder.\n    /// </summary>\n    /// <remarks>\n    /// Warning! To understand which ASP.NET services should be injected/removed by custom builder, see docs of the <see cref=\"OcelotBuilder.AddDefaultAspNetServices(IMvcCoreBuilder, Assembly)\"/> method.\n    /// </remarks>\n    /// <param name=\"services\">Current services collection.</param>\n    /// <param name=\"configuration\">Current web app configuration.</param>\n    /// <param name=\"customBuilder\">Current custom builder for ASP.NET MVC pipeline.</param>\n    /// <returns>An <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func<IMvcCoreBuilder, Assembly, IMvcCoreBuilder> customBuilder)\n    {\n        configuration ??= services.FindConfiguration(null);\n        return new OcelotBuilder(services, configuration, customBuilder);\n    }\n\n    private static IConfiguration DefaultConfiguration(IWebHostEnvironment env)\n        => new ConfigurationBuilder().AddOcelot(env).Build();\n\n    private static IConfiguration FindConfiguration(this IServiceCollection services, IWebHostEnvironment env)\n    {\n        var descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IConfiguration));\n        if (descriptor == null)\n        {\n            return DefaultConfiguration(env);\n        }\n\n        var provider = new ServiceCollection().Add(descriptor).BuildServiceProvider(true);\n        var configuration = provider.GetService<IConfiguration>();\n        return configuration ?? DefaultConfiguration(env);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Security.Claims;\n\nnamespace Ocelot.PathManipulation;\n\npublic class ChangeDownstreamPathTemplate : IChangeDownstreamPathTemplate\n{\n    private readonly IClaimsParser _claimsParser;\n\n    public ChangeDownstreamPathTemplate(IClaimsParser claimsParser)\n    {\n        _claimsParser = claimsParser;\n    }\n\n    public Response ChangeDownstreamPath(List<ClaimToThing> claimsToThings, IEnumerable<Claim> claims,\n        DownstreamPathTemplate downstreamPathTemplate, List<PlaceholderNameAndValue> placeholders)\n    {\n        foreach (var config in claimsToThings)\n        {\n            var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index);\n\n            if (value.IsError)\n            {\n                return new ErrorResponse(value.Errors);\n            }\n\n            var placeholderName = $\"{{{config.ExistingKey}}}\";\n\n            if (!downstreamPathTemplate.Value.Contains(placeholderName))\n            {\n                return new ErrorResponse(new CouldNotFindPlaceholderError(placeholderName));\n            }\n\n            if (placeholders.Any(ph => ph.Name == placeholderName))\n            {\n                placeholders.RemoveAll(ph => ph.Name == placeholderName);\n            }\n\n            placeholders.Add(new PlaceholderNameAndValue(placeholderName, value.Data));\n        }\n\n        return new OkResponse();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Security.Claims;\n\nnamespace Ocelot.PathManipulation;\n\npublic interface IChangeDownstreamPathTemplate\n{\n    Response ChangeDownstreamPath(List<ClaimToThing> claimsToThings, IEnumerable<Claim> claims,\n        DownstreamPathTemplate downstreamPathTemplate, List<PlaceholderNameAndValue> placeholders);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.PathManipulation;\r\n\r\nnamespace Ocelot.DownstreamPathManipulation.Middleware;\r\n\r\npublic class ClaimsToDownstreamPathMiddleware : OcelotMiddleware\r\n{\r\n    private readonly RequestDelegate _next;\r\n    private readonly IChangeDownstreamPathTemplate _changeDownstreamPathTemplate;\r\n\r\n    public ClaimsToDownstreamPathMiddleware(RequestDelegate next,\r\n        IOcelotLoggerFactory loggerFactory,\r\n        IChangeDownstreamPathTemplate changeDownstreamPathTemplate)\r\n            : base(loggerFactory.CreateLogger<ClaimsToDownstreamPathMiddleware>())\r\n    {\r\n        _next = next;\r\n        _changeDownstreamPathTemplate = changeDownstreamPathTemplate;\r\n    }\r\n\r\n    public async Task Invoke(HttpContext httpContext)\r\n    {\r\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\r\n\r\n        if (downstreamRoute.ClaimsToPath.Any())\r\n        {\r\n            Logger.LogInformation(() => $\"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path\");\r\n\r\n            var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues();\r\n\r\n            var response = _changeDownstreamPathTemplate.ChangeDownstreamPath(downstreamRoute.ClaimsToPath, httpContext.User.Claims,\r\n                downstreamRoute.DownstreamPathTemplate, templatePlaceholderNameAndValues);\r\n\r\n            if (response.IsError)\r\n            {\r\n                Logger.LogWarning(\"there was an error setting queries on context, setting pipeline error\");\r\n\r\n                httpContext.Items.UpsertErrors(response.Errors);\r\n                return;\r\n            }\r\n        }\r\n\r\n        await _next.Invoke(httpContext);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/DownstreamRouteHolder.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\n\nnamespace Ocelot.DownstreamRouteFinder;\n\npublic class DownstreamRouteHolder\n{\n    public DownstreamRouteHolder()\n    {\n    }\n\n    public DownstreamRouteHolder(List<PlaceholderNameAndValue> templatePlaceholderNameAndValues, Route route)\n    {\n        TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues;\n        Route = route;\n    }\n\n    public List<PlaceholderNameAndValue> TemplatePlaceholderNameAndValues { get; }\n    public Route Route { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/DiscoveryDownstreamRouteFinder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.Finder;\n\npublic class DiscoveryDownstreamRouteFinder : IDownstreamRouteProvider\n{\n    public const char Dot = '.';\n    public const char Slash = '/';\n    public const char Question = '?';\n\n    private readonly ConcurrentDictionary<string, OkResponse<DownstreamRouteHolder>> _cache;\n    private readonly IRouteKeyCreator _routeKeyCreator;\n    private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator;\n\n    public DiscoveryDownstreamRouteFinder(\n        IRouteKeyCreator routeKeyCreator,\n        IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator)\n    {\n        _cache = new();\n        _routeKeyCreator = routeKeyCreator;\n        _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;\n    }\n\n    public Response<DownstreamRouteHolder> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod,\n        IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders)\n    {\n        var serviceName = GetServiceName(upstreamUrlPath, out var serviceNamespace);\n        var downstreamPath = GetDownstreamPath(upstreamUrlPath);\n        var dynamicRoute = configuration.Routes\n            .Where(r => r.IsDynamic) // process dynamic routes only\n            .SelectMany(r => r.DownstreamRoute)\n            .FirstOrDefault(dr => dr.ServiceName == serviceName && (serviceNamespace.IsEmpty() || dr.ServiceNamespace == serviceNamespace));\n        var loadBalancerKey = dynamicRoute != null\n            ? dynamicRoute.LoadBalancerKey\n            : _routeKeyCreator.Create(serviceNamespace, serviceName, configuration.LoadBalancerOptions);\n        if (_cache.TryGetValue(loadBalancerKey, out var downstreamRouteHolder))\n        {\n            return downstreamRouteHolder;\n        }\n\n        // TODO: Could it be that the static route functionality was possibly lost here? -> StaticRoutesCreator.SetUpRoute -> _upstreamTemplatePatternCreator\n        var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build();\n        var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(upstreamHeaders, false); // ? discoveryDownstreamRoute.UpstreamHeaders\n\n        var routeBuilder = new DownstreamRouteBuilder()\n            .WithServiceName(serviceName)\n            .WithServiceNamespace(serviceNamespace)\n            .WithAuthenticationOptions(configuration.AuthenticationOptions)\n            .WithCacheOptions(configuration.CacheOptions)\n            .WithDownstreamHttpVersion(configuration.DownstreamHttpVersion)\n            .WithDownstreamHttpVersionPolicy(configuration.DownstreamHttpVersionPolicy)\n            .WithDownstreamPathTemplate(downstreamPath)\n            .WithDownstreamScheme(configuration.DownstreamScheme)\n            .WithHttpHandlerOptions(configuration.HttpHandlerOptions)\n            .WithLoadBalancerKey(loadBalancerKey)\n            .WithLoadBalancerOptions(configuration.LoadBalancerOptions)\n            .WithMetadata(configuration.MetadataOptions)\n            .WithQosOptions(configuration.QoSOptions)\n            .WithRateLimitOptions(configuration.RateLimitOptions)\n            .WithUpstreamHeaders(upstreamHeaderTemplates as Dictionary<string, UpstreamHeaderTemplate>)\n            .WithUpstreamPathTemplate(upstreamPathTemplate)\n            .WithTimeout(configuration.Timeout);\n        if (dynamicRoute != null)\n        {\n            // We are set to replace IInternalConfiguration global options with the current options from actual dynamic route\n            routeBuilder\n                .WithAuthenticationOptions(dynamicRoute.AuthenticationOptions)\n                .WithCacheOptions(dynamicRoute.CacheOptions)\n                .WithDownstreamHttpVersion(dynamicRoute.DownstreamHttpVersion)\n                .WithDownstreamHttpVersionPolicy(dynamicRoute.DownstreamHttpVersionPolicy)\n                .WithDownstreamScheme(dynamicRoute.DownstreamScheme)\n                .WithHttpHandlerOptions(dynamicRoute.HttpHandlerOptions)\n                .WithLoadBalancerKey(loadBalancerKey/*dynamicRoute.LoadBalancerKey*/)\n                .WithLoadBalancerOptions(dynamicRoute.LoadBalancerOptions)\n                .WithMetadata(dynamicRoute.MetadataOptions)\n                .WithQosOptions(dynamicRoute.QosOptions)\n                .WithRateLimitOptions(dynamicRoute.RateLimitOptions)\n                .WithServiceName(serviceName/*dynamicRoute.ServiceName*/)\n                .WithServiceNamespace(serviceNamespace/*dynamicRoute.ServiceNamespace*/)\n                .WithTimeout(dynamicRoute.Timeout);\n        }\n\n        var downstreamRoute = routeBuilder.Build();\n        var route = new Route(true, downstreamRoute) // IsDynamic -> true\n        {\n            UpstreamHeaderTemplates = upstreamHeaderTemplates,\n            UpstreamHost = upstreamHost,\n            UpstreamHttpMethod = [new(upstreamHttpMethod.Trim())],\n            UpstreamTemplatePattern = upstreamPathTemplate,\n        };\n        downstreamRouteHolder = new OkResponse<DownstreamRouteHolder>(new DownstreamRouteHolder(new List<PlaceholderNameAndValue>(), route));\n        _cache.AddOrUpdate(loadBalancerKey, downstreamRouteHolder, (x, y) => downstreamRouteHolder);\n        return downstreamRouteHolder;\n    }\n\n    private static string GetDownstreamPath(string upstreamUrlPath)\n    {\n        int index = upstreamUrlPath.IndexOf(Slash, 1);\n        return index != -1\n            ? upstreamUrlPath[index..]\n            : Slash.ToString();\n    }\n\n    /// <summary>Gets service name and its namespace of request URL.\n    /// <para>Note: A namespace and service name should be separated by a '.' (dot) character.</para></summary>\n    /// <remarks>Example: <c>http://ocelot.net/namespace.service-name/path</c> URL.</remarks>\n    /// <param name=\"upstreamUrlPath\">The upstream path.</param>\n    /// <param name=\"serviceNamespace\">Extracted namespace.</param>\n    /// <returns>A <see cref=\"string\"/> object.</returns>\n    protected virtual string GetServiceName(string upstreamUrlPath, out string serviceNamespace)\n    {\n        var path = upstreamUrlPath.AsSpan();\n        int index = path[1..].IndexOf(Slash);\n        var name = index == -1\n            ? path[1..]\n            : path.Slice(1, index).TrimEnd(Slash);\n\n        index = name.IndexOf(Dot);\n        serviceNamespace = index == -1\n            ? string.Empty\n            : name[..index].ToString();\n        var serviceName = index == -1 ? name : name[++index..];\n        return serviceName.ToString();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\r\nusing Ocelot.Responses;\r\n\r\nnamespace Ocelot.DownstreamRouteFinder.Finder;\r\n\r\npublic class DownstreamRouteFinder : IDownstreamRouteProvider\r\n{\r\n    private readonly IUrlPathToUrlTemplateMatcher _urlMatcher;\r\n    private readonly IPlaceholderNameAndValueFinder _pathPlaceholderFinder;\n    private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher;\n    private readonly IHeaderPlaceholderNameAndValueFinder _headerPlaceholderFinder;\r\n\r\n    public DownstreamRouteFinder(\n        IUrlPathToUrlTemplateMatcher urlMatcher,\n        IPlaceholderNameAndValueFinder pathPlaceholderFinder,\n        IHeadersToHeaderTemplatesMatcher headerMatcher,\n        IHeaderPlaceholderNameAndValueFinder headerPlaceholderFinder)\r\n    {\r\n        _urlMatcher = urlMatcher;\r\n        _pathPlaceholderFinder = pathPlaceholderFinder;\n        _headerMatcher = headerMatcher;\n        _headerPlaceholderFinder = headerPlaceholderFinder;\r\n    }\r\n\r\n    public Response<DownstreamRouteHolder> Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod,\n        IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders)\r\n    {\r\n        var downstreamRoutes = new List<DownstreamRouteHolder>();\r\n\r\n        var applicableRoutes = configuration.Routes\r\n            .Where(r => !r.IsDynamic && RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) // process static routes only\r\n            .OrderByDescending(x => x.UpstreamTemplatePattern.Priority);\r\n\r\n        foreach (var route in applicableRoutes)\r\n        {\r\n            var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern);\n            var headersMatch = _headerMatcher.Match(upstreamHeaders, route.UpstreamHeaderTemplates);\r\n            if (urlMatch.Match && headersMatch)\r\n            {\r\n                downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route, upstreamHeaders));\r\n            }\r\n        }\n\n        if (downstreamRoutes.Count != 0)\r\n        {\r\n            var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.Route.UpstreamHost));\r\n            var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost));\n            return new OkResponse<DownstreamRouteHolder>(notNullOption ?? nullOption);\r\n        }\r\n\r\n        return new ErrorResponse<DownstreamRouteHolder>(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod));\r\n    }\r\n\r\n    private static bool RouteIsApplicableToThisRequest(Route route, string httpMethod, string upstreamHost)\r\n    {\r\n        var method = new HttpMethod(httpMethod.Trim());\r\n        return (route.UpstreamHttpMethod.Count == 0 || route.UpstreamHttpMethod.Contains(method))\r\n                &&\r\n               (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost);\r\n    }\r\n\r\n    private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route, IHeaderDictionary upstreamHeaders)\r\n    {\r\n        var templatePlaceholderNameAndValues = _pathPlaceholderFinder\n            .Find(path, query, route.UpstreamTemplatePattern.OriginalValue)\n            .Data;\n        var headerPlaceholders = _headerPlaceholderFinder.Find(upstreamHeaders, route.UpstreamHeaderTemplates);\n        templatePlaceholderNameAndValues.AddRange(headerPlaceholders);\r\n\r\n        return new DownstreamRouteHolder(templatePlaceholderNameAndValues, route);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Logging;\r\n\r\nnamespace Ocelot.DownstreamRouteFinder.Finder;\r\n\r\npublic class DownstreamRouteProviderFactory : IDownstreamRouteProviderFactory\r\n{\r\n    private readonly Dictionary<string, IDownstreamRouteProvider> _providers; // TODO We need to use a HashSet<int> here for quicker lookups\r\n    private readonly IOcelotLogger _logger;\r\n\r\n    public DownstreamRouteProviderFactory(IServiceProvider provider, IOcelotLoggerFactory factory)\r\n    {\r\n        _logger = factory.CreateLogger<DownstreamRouteProviderFactory>();\r\n        _providers = provider.GetServices<IDownstreamRouteProvider>().ToDictionary(x => x.GetType().Name);\r\n    }\r\n\r\n    public IDownstreamRouteProvider Get(IInternalConfiguration config)\r\n    {\r\n        //todo - this is a bit hacky we are saying there are no routes or there are routes but none of them have\r\n        //an upstream path template which means they are dyanmic and service discovery is on...\r\n        if ((config.Routes.Length == 0 || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration))\r\n        {\r\n            _logger.LogInformation($\"Selected {nameof(DiscoveryDownstreamRouteFinder)} as {nameof(IDownstreamRouteProvider)} for this request\");\r\n\r\n            return _providers[nameof(DiscoveryDownstreamRouteFinder)];\r\n        }\r\n\r\n        return _providers[nameof(DownstreamRouteFinder)];\r\n    }\r\n\r\n    private static bool IsServiceDiscovery(ServiceProviderConfiguration config)\r\n    {\r\n        return !string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config?.Type);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Responses;\r\n\nnamespace Ocelot.DownstreamRouteFinder.Finder;\r\n\r\npublic interface IDownstreamRouteProvider\r\n{\r\n    Response<DownstreamRouteHolder> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod,\n        IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.DownstreamRouteFinder.Finder;\n\npublic interface IDownstreamRouteProviderFactory\n{\n    IDownstreamRouteProvider Get(IInternalConfiguration config);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs",
    "content": "﻿using Ocelot.Errors;\nusing Status = System.Net.HttpStatusCode;\n\nnamespace Ocelot.DownstreamRouteFinder.Finder;\n\npublic class UnableToFindDownstreamRouteError : Error\n{\n    public UnableToFindDownstreamRouteError(string path, string httpVerb)\n        : base($\"Failed to match route configuration for upstream: {httpVerb} {path}\", OcelotErrorCode.UnableToFindDownstreamRouteError, (int)Status.NotFound)\n    { }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.HeaderMatcher;\n\npublic class HeaderPlaceholderNameAndValueFinder : IHeaderPlaceholderNameAndValueFinder\n{\n    public IList<PlaceholderNameAndValue> Find(IHeaderDictionary upstreamHeaders, IDictionary<string, UpstreamHeaderTemplate> templateHeaders)\n    {\n        var result = new List<PlaceholderNameAndValue>();\n        foreach (var templateHeader in templateHeaders)\n        {\n            var upstreamHeader = upstreamHeaders[templateHeader.Key];\n            var matches = templateHeader.Value.Pattern.Matches(upstreamHeader);\n            var placeholders = matches\n                .SelectMany(g => g.Groups as IEnumerable<Group>)\n                .Where(g => g.Name != \"0\")\n                .Select(g => new PlaceholderNameAndValue(string.Concat('{', g.Name, '}'), g.Value));\n            result.AddRange(placeholders);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.HeaderMatcher;\n\npublic class HeadersToHeaderTemplatesMatcher : IHeadersToHeaderTemplatesMatcher\n{\n    public bool Match(IHeaderDictionary upstreamHeaders, IDictionary<string, UpstreamHeaderTemplate> routeHeaders)\n    {\n        bool IsMatching(KeyValuePair<string, UpstreamHeaderTemplate> h)\n        {\n            return upstreamHeaders.TryGetValue(h.Key, out StringValues header)\n                && routeHeaders[h.Key].Pattern.IsMatch(header);\n        }\n        return routeHeaders == null || upstreamHeaders != null && routeHeaders.All(IsMatching);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.HeaderMatcher;\n\n/// <summary>\n/// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers\">Routing based on request header</see>.\n/// </summary>\npublic interface IHeaderPlaceholderNameAndValueFinder\n{\n    IList<PlaceholderNameAndValue> Find(IHeaderDictionary upstreamHeaders, IDictionary<string, UpstreamHeaderTemplate> templateHeaders);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.HeaderMatcher;\n\n/// <summary>\n/// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers\">Routing based on request header</see>.\n/// </summary>\npublic interface IHeadersToHeaderTemplatesMatcher\n{\n    bool Match(IHeaderDictionary upstreamHeaders, IDictionary<string, UpstreamHeaderTemplate> routeHeaders);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.DownstreamRouteFinder.Middleware;\n\npublic class DownstreamRouteFinderMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IDownstreamRouteProviderFactory _factory;\n\n    public DownstreamRouteFinderMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IDownstreamRouteProviderFactory downstreamRouteFinder)\n        : base(loggerFactory.CreateLogger<DownstreamRouteFinderMiddleware>())\n    {\n        _next = next;\n        _factory = downstreamRouteFinder;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var upstreamUrlPath = httpContext.Request.Path.ToString();\n        var upstreamQueryString = httpContext.Request.QueryString.ToString();\n        var internalConfiguration = httpContext.Items.IInternalConfiguration();\n        var hostHeader = httpContext.Request.Headers.Host.ToString();\n        var upstreamHost = hostHeader.Contains(':')\n            ? hostHeader.Split(':')[0]\n            : hostHeader;\n        var upstreamHeaders = httpContext.Request.Headers;\n\n        Logger.LogDebug(() => $\"Upstream URL path: {upstreamUrlPath}\");\n\n        var provider = _factory.Get(internalConfiguration);\n        var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost, upstreamHeaders);\n        if (response.IsError)\n        {\n            Logger.LogWarning(() => $\"{MiddlewareName} setting pipeline errors because {provider.GetType().Name} returned the following ->{response.Errors.ToErrorString(true)}\");\n            httpContext.Items.UpsertErrors(response.Errors);\n            return;\n        }\n\n        Logger.LogDebug(() => $\"Downstream templates: {string.Join(\", \", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}\");\n\n        // why set both of these on HttpContext\n        httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues);\n        httpContext.Items.UpsertDownstreamRoute(response.Data);\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs",
    "content": "﻿using Ocelot.Responses;\n\nnamespace Ocelot.DownstreamRouteFinder.UrlMatcher;\n\npublic interface IPlaceholderNameAndValueFinder\n{\n    Response<List<PlaceholderNameAndValue>> Find(string path, string query, string pathTemplate);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathToUrlTemplateMatcher.cs",
    "content": "using Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.UrlMatcher;\n\npublic interface IUrlPathToUrlTemplateMatcher\n{\n    UrlMatch Match(string upstreamUrlPath, string upstreamQueryString, UpstreamPathTemplate pathTemplate);\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs",
    "content": "using Ocelot.Infrastructure;\n\nnamespace Ocelot.DownstreamRouteFinder.UrlMatcher;\n\npublic class PlaceholderNameAndValue\n{\n    private const char OpeningBrace = Placeholders.OpeningBrace;\n    private const char ClosingBrace = Placeholders.ClosingBrace;\n\n    public PlaceholderNameAndValue(string name, string value)\n    {\n        Name = name;\n        Value = value;\n    }\n\n    public string Name { get; }\n    public string Value { get; }\n\n    public string Key { get => Name.Trim(OpeningBrace, ClosingBrace); }\n    public override string ToString() => $\"[{Name}={Value}]\";\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs",
    "content": "﻿using Ocelot.Values;\n\nnamespace Ocelot.DownstreamRouteFinder.UrlMatcher;\n\npublic class RegExUrlMatcher : IUrlPathToUrlTemplateMatcher\n{\n    public UrlMatch Match(string upstreamUrlPath, string upstreamQueryString, UpstreamPathTemplate pathTemplate)\n        => !pathTemplate.ContainsQueryString\n            ? new(pathTemplate.Pattern.IsMatch(upstreamUrlPath))\n            : new(pathTemplate.Pattern.IsMatch($\"{upstreamUrlPath}{upstreamQueryString}\"));\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs",
    "content": "namespace Ocelot.DownstreamRouteFinder.UrlMatcher;\r\n\r\npublic class UrlMatch\r\n{\r\n    public UrlMatch(bool match)\r\n    {\r\n        Match = match;\n    }\r\n\r\n    public bool Match { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs",
    "content": "using Ocelot.Infrastructure;\nusing Ocelot.Responses;\n\nnamespace Ocelot.DownstreamRouteFinder.UrlMatcher;\n\n/// <summary>The finder locates all occurrences of placeholders' names and values within URL paths.\n/// <para>This is the default implementation of the <see cref=\"IPlaceholderNameAndValueFinder\"/> interface.</para>\n/// </summary>\npublic partial class UrlPathPlaceholderNameAndValueFinder : IPlaceholderNameAndValueFinder\n{\n    private const char LeftBrace = '{';\n    private const char RightBrace = '}';\n\n    /// <summary>Finds the placeholders in the request path and query and returns their matching values.\n    /// <para>We might encounter the following scenarios:\n    /// <list type=\"bullet\">\n    /// <item>The path template contains a Catch-All query parameter. If so, we return the Catch-All placeholder with an empty value.</item>\n    /// <item>The path template contains a Catch-All path parameter. If so, we return the Catch-All placeholder with an empty value.</item>\n    /// <item>The path template contains placeholders. We return the placeholders with their matching values.</item>\n    /// </list>\n    /// </para>\n    /// </summary>\n    /// <param name=\"path\">The request path.</param>\n    /// <param name=\"query\">The query parameters.</param>\n    /// <param name=\"pathTemplate\">The request path template.</param>\n    /// <returns>A <see cref=\"List{PlaceholderNameAndValue}\"/> object, where T is <see cref=\"PlaceholderNameAndValue\"/>: the list of the placeholders with their matching values.</returns>\n    public Response<List<PlaceholderNameAndValue>> Find(string path, string query, string pathTemplate)\n    {\n        (bool isCatchAllQuery, string catchAllQueryPlaceholder) = IsCatchAllQuery(pathTemplate);\n        if (isCatchAllQuery)\n        {\n            return new OkResponse<List<PlaceholderNameAndValue>>(new List<PlaceholderNameAndValue>\n            {\n                new($\"{LeftBrace}{catchAllQueryPlaceholder}{RightBrace}\", string.Empty),\n            });\n        }\n\n        // Find matching groups from path and query\n        var placeholders = FindGroups(path, query, pathTemplate)\n            .Select(g => new PlaceholderNameAndValue($\"{LeftBrace}{g.Name}{RightBrace}\", g.Value))\n            .ToList();\n        return new OkResponse<List<PlaceholderNameAndValue>>(placeholders);\n    }\n\n    private const int PlaceholdersMilliseconds = 1000;\n    [GeneratedRegex(@\"\\{(.*?)\\}\", RegexOptions.None, PlaceholdersMilliseconds)]\n    private static partial Regex RegexPlaceholders();\n\n    /// <summary>Finds the placeholders in the request path and query.\n    /// We use a <see cref=\"Regex\"/> pattern to match the placeholders in the path template.\n    /// <para>We have two slight optimizations:\n    /// <list type=\"number\">\n    /// <item>First, we skip the query if it is not present in the path template.</item>\n    /// <item>Second, we append a trailing slash to the path if it is a Catch-All path.</item>\n    /// </list>\n    /// </para>\n    /// </summary>\n    /// <param name=\"path\">The request path.</param>\n    /// <param name=\"query\">The query parameters.</param>\n    /// <param name=\"template\">The path template.</param>\n    /// <returns>A <see cref=\"List{Group}\"/> object (T is <see cref=\"Group\"/>): the matching groups.</returns>\n    private static List<Group> FindGroups(string path, string query, string template)\n    {\n        template = EscapeExceptBraces(template);\n        var regexPattern = GenerateRegexPattern(template);\n        var testedPath = ShouldSkipQuery(query, template) ? path : $\"{path}{query}\";\n        \n        var match = Regex.Match(testedPath, regexPattern);\n        var foundGroups = match.Groups.Cast<Group>().Skip(1).ToList();\n        \n        if (foundGroups.Count > 0 || !IsCatchAllPath(template))\n        {\n            return foundGroups;\n        }\n\n        // Append a trailing slash to the path if it is a catch-all path\n        match = Regex.Match($\"{testedPath}/\", regexPattern);\n        return match.Groups.Cast<Group>().Skip(1).ToList();\n    }\n    \n    /// <summary>\n    /// The placeholders that are not placed at the end of the template are delimited by forward slashes, only the last one, the catch-all can match more segments.\n    /// </summary>\n    /// <param name=\"escapedTemplate\">The escaped path template.</param>\n    /// <returns>The pattern for values replacement.</returns>\n    private static string GenerateRegexPattern(string escapedTemplate)\n    {\n        // First we count the matches\n        var placeHoldersCountMatch = RegexPlaceholders().Matches(escapedTemplate);\n        int index = 0, placeHoldersCount = placeHoldersCountMatch.Count;\n\n        // We know that the replace process will be started from the beginning of the url,\n        // so we can use a simple counter to determine the last placeholder\n        string MatchEvaluator(Match match)\n        {\n            var groupName = match.Groups[1].Value;\n            index++;\n            return index == placeHoldersCount ? $\"(?<{groupName}>[^&]*)\" : $\"(?<{groupName}>[^/|&]*)\";\n        }\n\n        return $@\"^{RegexPlaceholders().Replace(escapedTemplate, MatchEvaluator)}\";\n    }\n\n    private const int CatchAllQueryMilliseconds = 300;\n    [GeneratedRegex(@\"^[^{{}}]*\\?\\{(.*?)\\}$\", RegexOptions.None, CatchAllQueryMilliseconds)]\n    private static partial Regex RegexCatchAllQuery();\n\n    /// <summary>Checks if the path template contains a Catch-All query parameter.\n    /// <para>It means that the path template ends with a question mark and a placeholder.\n    /// And no other placeholders are present in the path template.</para>\n    /// </summary>\n    /// <param name=\"template\">The path template.</param>\n    /// <returns><see langword=\"true\"/> if it matches and the found placeholder.</returns>\n    private static (bool IsMatch, string Placeholder) IsCatchAllQuery(string template)\n    {\n        var catchAllMatch = RegexCatchAllQuery().Match(template);\n        return (catchAllMatch.Success,\n            catchAllMatch.Success ? catchAllMatch.Groups[1].Value : string.Empty);\n    }\n\n    private const int CatchAllPathMilliseconds = 300;\n    [GeneratedRegex(@\"^[^{{}}]*\\{(.*?)\\}/?$\", RegexOptions.None, CatchAllPathMilliseconds)]\n    private static partial Regex RegexCatchAllPath();\n\n    /// <summary>Check if the path template contains a Catch-All path parameter.\n    /// <para>It means that the path template ends with a placeholder and no other placeholders are present in the path template, without a question mark (query parameters).</para>\n    /// </summary>\n    /// <param name=\"template\">The path template.</param>\n    /// <returns><see langword=\"true\"/> if it matches.</returns>\n    private static bool IsCatchAllPath(string template) => RegexCatchAllPath().IsMatch(template) && !template.Contains(@\"\\?\");\n\n    /// <summary>Checks if the query should be skipped.\n    /// <para>It should be skipped if it is not present in the path template.\n    /// Since the template is escaped, looking for \\? not only ?.</para>\n    /// </summary>\n    /// <param name=\"query\">The query string.</param>\n    /// <param name=\"template\">The path template.</param>\n    /// <returns><see langword=\"true\"/> if query should be skipped.</returns>\n    private static bool ShouldSkipQuery(string query, string template) => !string.IsNullOrEmpty(query) && !template.Contains(@\"\\?\");\n\n    /// <summary>Escapes all characters except braces, eg { and }.</summary>\n    /// <param name=\"input\">The input string.</param>\n    /// <returns>The formatted <see cref=\"string\"/>.</returns>\n    private static string EscapeExceptBraces(string input)\n    {\n        if (string.IsNullOrEmpty(input))\n        {\n            return string.Empty;\n        }\n\n        StringBuilder escaped = new();\n        foreach (char c in input.AsSpan())\n        {\n            if (c is LeftBrace or RightBrace)\n            {\n                escaped.Append(c);\n            }\n            else\n            {\n                escaped.Append(Regex.Escape(c.ToString()));\n            }\n        }\n\n        // Here we are not interested in the path itself, only the placeholders.\n        // Path validation is not part of this class, therefore allowing case-insensitive \n        // matching.\n        return $\"^(?i){escaped}\";\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs",
    "content": "using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamUrlCreator;\n\n/// <summary>\n/// TODO Move this service to the middleware as a protected virtual method. Having a separate interface is absolutely useless.\n/// </summary>\npublic class DownstreamPathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer\n{\n    public DownstreamPath Replace(string downstreamPathTemplate, List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues)\n    {\n        var downstreamPath = new StringBuilder(downstreamPathTemplate);\n        foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues)\n        {\n            downstreamPath.Replace(placeholderVariableAndValue.Name, placeholderVariableAndValue.Value);\n        }\n\n        return new(downstreamPath.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamUrlCreator/DownstreamUrlCreatorMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.WebUtilities;\nusing Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\nusing System.Web;\n\nnamespace Ocelot.DownstreamUrlCreator;\n\npublic class DownstreamUrlCreatorMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IDownstreamPathPlaceholderReplacer _replacer;\n\n    private const char Ampersand = '&';\n    private const char QuestionMark = '?';\n    protected const char Slash = '/';\n\n    public DownstreamUrlCreatorMiddleware(\n        RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IDownstreamPathPlaceholderReplacer replacer)\n        : base(loggerFactory.CreateLogger<DownstreamUrlCreatorMiddleware>())\n    {\n        _next = next;\n        _replacer = replacer;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        var downstreamRoute = context.Items.DownstreamRoute();\n        var placeholders = context.Items.TemplatePlaceholderNameAndValues();\n        var downstreamPath = _replacer.Replace(downstreamRoute.DownstreamPathTemplate.Value, placeholders);\n        if (downstreamPath.Value.IsEmpty())\n        {\n            throw new NotSupportedException($\"{_replacer.GetType().Name} returned an empty {nameof(DownstreamPath)} for the route {downstreamRoute.Name()}.\");\n        }\n\n        var dsPath = downstreamPath.Value;\n        var downstreamRequest = context.Items.DownstreamRequest();\n        var upstreamPath = downstreamRequest.AbsolutePath;\n        if (dsPath.EndsWith(Slash) && !upstreamPath.EndsWith(Slash))\n        {\n            dsPath = dsPath.TrimEnd(Slash);\n            downstreamPath = new DownstreamPath(dsPath);\n        }\n\n        if (!string.IsNullOrEmpty(downstreamRoute.DownstreamScheme))\n        {\n            // TODO Make sure this works, hopefully there is a test ;E\n            context.Items.DownstreamRequest().Scheme = downstreamRoute.DownstreamScheme;\n        }\n\n        var internalConfiguration = context.Items.IInternalConfiguration();\n\n        if (ServiceFabricRequest(internalConfiguration, downstreamRoute))\n        {\n            var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, placeholders, downstreamPath);\n\n            // TODO Check this works again hope there is a test..\n            downstreamRequest.AbsolutePath = path;\n            downstreamRequest.Query = query;\n        }\n        else\n        {\n            if (dsPath.Contains(QuestionMark))\n            {\n                downstreamRequest.AbsolutePath = GetPath(dsPath).ToString();\n                var newQuery = GetQueryString(dsPath).ToString();\n                downstreamRequest.Query = string.IsNullOrEmpty(downstreamRequest.Query)\n                    ? newQuery\n                    : MergeQueryStringsWithoutDuplicateValues(downstreamRequest.Query, newQuery, placeholders);\n            }\n            else\n            {\n                downstreamRequest.AbsolutePath = dsPath;\n                downstreamRequest.Query = RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, placeholders);\n            }\n        }\n\n        Logger.LogDebug(() => $\"Downstream URL: {downstreamRequest}\");\n\n        await _next.Invoke(context);\n    }\n\n    /// <summary>\n    /// <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#merging-of-query-parameters\">Merging of Query Parameters</see> is part of\n    /// the <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#query-placeholders\">Query Placeholders</see> feature.\n    /// </summary>\n    /// <returns>A <see cref=\"string\"/> object.</returns>\n    protected static string MergeQueryStringsWithoutDuplicateValues(string queryString, string newQueryString, List<PlaceholderNameAndValue> placeholders)\n    {\n        newQueryString = newQueryString.Replace(QuestionMark, Ampersand);\n        var queries = HttpUtility.ParseQueryString(queryString);\n        var newQueries = HttpUtility.ParseQueryString(newQueryString);\n\n        // Remove old replaced query parameters\n        var placeholderKeys = new HashSet<string>(placeholders.Select(p => p.Key));\n        foreach (var queryKey in queries.AllKeys.Where(placeholderKeys.Contains))\n        {\n            queries.Remove(queryKey);\n        }\n\n        var parameters = newQueries.AllKeys\n            .Where(key => key.IsNotEmpty())\n            .ToDictionary(key => key, key => newQueries[key]);\n\n        _ = queries.AllKeys\n            .Where(key => key.IsNotEmpty() && !parameters.ContainsKey(key))\n            .All(key => parameters.TryAdd(key, queries[key]));\n\n        return QueryHelpers.AddQueryString(string.Empty, parameters);\n    }\n\n    /// <summary>\n    /// Feature 467: <see href=\"https://github.com/ThreeMammals/Ocelot/pull/467\">Added support for query string parameters in upstream path template</see>.\n    /// </summary>\n    /// <returns>A <see cref=\"string\"/> object without wanted parameters.</returns>\n    protected static string RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest request, List<PlaceholderNameAndValue> templatePlaceholders)\n    {\n        if (templatePlaceholders.Count == 0 || request.Query.IsEmpty())\n        {\n            return request.Query;\n        }\n\n        var query = QueryHelpers.ParseQuery(request.Query);\n        foreach (var placeholder in templatePlaceholders.Where(p => query.ContainsKey(p.Key)))\n        {\n            query.Remove(placeholder.Key);\n        }\n\n        return QueryHelpers.AddQueryString(string.Empty, query);\n    }\n\n    protected static ReadOnlySpan<char> GetPath(ReadOnlySpan<char> downstreamPath)\n    {\n        int length = downstreamPath.IndexOf(QuestionMark);\n        return length >= 0\n            ? downstreamPath[..length]\n            : downstreamPath;\n    }\n\n    protected static ReadOnlySpan<char> GetQueryString(ReadOnlySpan<char> downstreamPath)\n    {\n        int startIndex = downstreamPath.IndexOf(QuestionMark);\n        return startIndex >= 0\n            ? downstreamPath[startIndex..]\n            : ReadOnlySpan<char>.Empty;\n    }\n\n    protected (string Path, string Query) CreateServiceFabricUri(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute, List<PlaceholderNameAndValue> templatePlaceholderNameAndValues, DownstreamPath dsPath)\n    {\n        var query = downstreamRequest.Query;\n        var serviceName = _replacer.Replace(downstreamRoute.ServiceName, templatePlaceholderNameAndValues);\n        var pathTemplate = $\"/{serviceName.Value}{dsPath.Value}\";\n        return (pathTemplate, query);\n    }\n\n    protected static bool ServiceFabricRequest(IInternalConfiguration config, DownstreamRoute route)\n        => ServiceFabricServiceDiscoveryProvider.Type.Equals(config.ServiceProviderConfiguration.Type, StringComparison.OrdinalIgnoreCase)\n            && route.UseServiceDiscovery;\n}\n"
  },
  {
    "path": "src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs",
    "content": "using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.DownstreamUrlCreator;\n\npublic interface IDownstreamPathPlaceholderReplacer\n{\n    DownstreamPath Replace(string downstreamPathTemplate, List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues);\n}\n"
  },
  {
    "path": "src/Ocelot/Errors/Error.cs",
    "content": "namespace Ocelot.Errors;\r\n\r\npublic abstract class Error\n{\r\n    protected Error(string message, OcelotErrorCode code, int httpStatusCode)\r\n    {\n        HttpStatusCode = httpStatusCode;\r\n        Message = message;\r\n        Code = code;\r\n    }\r\n\r\n    public string Message { get; }\r\n    public OcelotErrorCode Code { get; }\n    public int HttpStatusCode { get; }\r\n\r\n    public override string ToString() => $\"{Code}: {Message}\";\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Errors.Middleware;\n\n/// <summary>\n/// Catches all unhandled exceptions thrown by middleware, logs and returns a 500.\n/// </summary>\npublic class ExceptionHandlerMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IRequestScopedDataRepository _repo;\n\n    public ExceptionHandlerMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IRequestScopedDataRepository repo)\n            : base(loggerFactory.CreateLogger<ExceptionHandlerMiddleware>())\n    {\n        _next = next;\n        _repo = repo;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        try\n        {\n            context.RequestAborted.ThrowIfCancellationRequested();\n\n            var configuration = context.Items.IInternalConfiguration();\n            TrySetGlobalRequestId(context, configuration);\n\n            Logger.LogDebug(\"Ocelot pipeline started\");\n            await _next.Invoke(context);\n        }\n        catch (OperationCanceledException e) when (context.RequestAborted.IsCancellationRequested)\n        {\n            Logger.LogDebug(\"Operation canceled\");\n            Logger.LogWarning(() => CreateMessage(context, e, true));\n            if (!context.Response.HasStarted)\n            {\n                context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest; // custom Ocelot code\n            }\n        }\n        catch (Exception e)\n        {\n            Logger.LogDebug(\"Error calling middleware\");\n            Logger.LogError(() => CreateMessage(context, e), e);\n            if (!context.Response.HasStarted)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;\n            }\n        }\n        finally\n        {\n            Logger.LogDebug(\"Ocelot pipeline finished\");\n        }\n    }\n\n    private void TrySetGlobalRequestId(HttpContext context, IInternalConfiguration configuration)\n    {\n        var key = configuration.RequestId;\n        if (!string.IsNullOrEmpty(key) && context.Request.Headers.TryGetValue(key, out var upstreamRequestIds))\n        {\n            context.TraceIdentifier = upstreamRequestIds.First();\n        }\n\n        _repo.Add(nameof(IInternalConfiguration.RequestId), context.TraceIdentifier);\n    }\n\n    private static string CreateMessage(HttpContext context, Exception e, bool includeException = false)\n    {\n        var original = e;\n        var builder = new StringBuilder()\n            .AppendLine($\"{e.GetType().Name} caught in global error handler!\");\n        int total = 0;\n        while (e.InnerException != null)\n        {\n            builder.AppendLine(e.InnerException.ToString());\n            e = e.InnerException;\n            total++;\n        }\n\n        builder.Append($\"DONE reporting of a total {total} inner exception{total.Plural()} for request {context.TraceIdentifier} of the original {original.GetType().Name} below ->\");\n        if (includeException)\n        {\n            builder.Append(Environment.NewLine + original.ToString());\n        }\n\n        return builder.ToString();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Errors/OcelotErrorCode.cs",
    "content": "﻿namespace Ocelot.Errors;\n\npublic enum OcelotErrorCode\n{\n    UnauthenticatedError = 0,\n    UnknownError = 1,\n    DownstreampathTemplateAlreadyUsedError = 2,\n    UnableToFindDownstreamRouteError = 3,\n    CannotAddDataError = 4,\n    CannotFindDataError = 5,\n    UnableToCompleteRequestError = 6,\n    UnableToCreateAuthenticationHandlerError = 7,\n    UnsupportedAuthenticationProviderError = 8,\n    CannotFindClaimError = 9,\n    ParsingConfigurationHeaderError = 10,\n    NoInstructionsError = 11,\n    InstructionNotForClaimsError = 12,\n    UnauthorizedError = 13,\n    ClaimValueNotAuthorizedError = 14,\n    ScopeNotAuthorizedError = 15,\n    UserDoesNotHaveClaimError = 16,\n    DownstreamPathTemplateContainsSchemeError = 17,\n    DownstreamPathNullOrEmptyError = 18,\n    DownstreamSchemeNullOrEmptyError = 19,\n    DownstreamHostNullOrEmptyError = 20,\n    ServicesAreNullError = 21,\n    ServicesAreEmptyError = 22,\n    UnableToFindServiceDiscoveryProviderError = 23,\n    UnableToFindLoadBalancerError = 24,\n    RequestTimedOutError = 25,\n    UnableToFindQoSProviderError = 26,\n    UnmappableRequestError = 27,\n    RateLimitOptionsError = 28,\n    PathTemplateDoesntStartWithForwardSlash = 29,\n    FileValidationFailedError = 30,\n    UnableToFindDelegatingHandlerProviderError = 31,\n    CouldNotFindPlaceholderError = 32,\n    CouldNotFindAggregatorError = 33,\n    CannotAddPlaceholderError = 34,\n    CannotRemovePlaceholderError = 35,\n    QuotaExceededError = 36,\n    RequestCanceled = 37,\n    ConnectionToDownstreamServiceError = 38,\n    CouldNotFindLoadBalancerCreator = 39,\n    ErrorInvokingLoadBalancerCreator = 40,\n    PayloadTooLargeError = 41,\n}\n"
  },
  {
    "path": "src/Ocelot/Errors/RequestTimedOutError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.Errors;\n\npublic class RequestTimedOutError : Error\n{\n    public RequestTimedOutError(Exception exception)\n        : base($\"Timeout making http request, exception: {exception}\",\n            OcelotErrorCode.RequestTimedOutError, StatusCodes.Status503ServiceUnavailable)\n    { }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/AddHeadersToRequest.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Logging;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Headers;\n\npublic class AddHeadersToRequest : IAddHeadersToRequest\n{\n    private readonly IClaimsParser _claimsParser;\n    private readonly IPlaceholders _placeholders;\n    private readonly IOcelotLogger _logger;\n\n    public AddHeadersToRequest(IClaimsParser claimsParser, IPlaceholders placeholders, IOcelotLoggerFactory factory)\n    {\n        _logger = factory.CreateLogger<AddHeadersToRequest>();\n        _claimsParser = claimsParser;\n        _placeholders = placeholders;\n    }\n\n    public Response SetHeadersOnDownstreamRequest(List<ClaimToThing> claimsToThings, IEnumerable<System.Security.Claims.Claim> claims, DownstreamRequest downstreamRequest)\n    {\n        foreach (var config in claimsToThings)\n        {\n            var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index);\n\n            if (value.IsError)\n            {\n                return new ErrorResponse(value.Errors);\n            }\n\n            var exists = downstreamRequest.Headers.FirstOrDefault(x => x.Key == config.ExistingKey);\n\n            if (!string.IsNullOrEmpty(exists.Key))\n            {\n                downstreamRequest.Headers.Remove(exists.Key);\n            }\n\n            downstreamRequest.Headers.Add(config.ExistingKey, value.Data);\n        }\n\n        return new OkResponse();\n    }\n\n    public void SetHeadersOnDownstreamRequest(IEnumerable<AddHeader> headers, HttpContext context)\n    {\n        var requestHeader = context.Request.Headers;\n\n        foreach (var header in headers)\n        {\n            if (requestHeader.ContainsKey(header.Key))\n            {\n                requestHeader.Remove(header.Key);\n            }\n\n            if (header.Value.StartsWith('{') && header.Value.EndsWith(\"}\"))\n            {\n                var value = _placeholders.Get(header.Value);\n\n                if (value.IsError)\n                {\n                    _logger.LogWarning(() => $\"Unable to add header to response {header.Key}: {header.Value}\");\n                    continue;\n                }\n\n                requestHeader.Append(header.Key, new StringValues(value.Data));\n            }\n            else\n            {\n                requestHeader.Append(header.Key, header.Value);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/AddHeadersToResponse.cs",
    "content": "using Ocelot.Configuration.Creator;\nusing Ocelot.Infrastructure;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Headers;\n\npublic class AddHeadersToResponse : IAddHeadersToResponse\n{\n    private readonly IPlaceholders _placeholders;\n    private readonly IOcelotLogger _logger;\n\n    public AddHeadersToResponse(IPlaceholders placeholders, IOcelotLoggerFactory factory)\n    {\n        _logger = factory.CreateLogger<AddHeadersToResponse>();\n        _placeholders = placeholders;\n    }\n\n    public void Add(List<AddHeader> addHeaders, DownstreamResponse response)\n    {\n        foreach (var add in addHeaders)\n        {\n            if (add.Value.StartsWith('{') && add.Value.EndsWith('}'))\n            {\n                var value = _placeholders.Get(add.Value);\n\n                if (value.IsError)\n                {\n                    _logger.LogWarning(() => $\"Unable to add header to response {add.Key}: {add.Value}\");\n                    continue;\n                }\n\n                response.Headers.Add(new Header(add.Key, new List<string> { value.Data }));\n            }\n            else\n            {\n                response.Headers.Add(new Header(add.Key, new List<string> { add.Value }));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Headers;\n\npublic class HttpContextRequestHeaderReplacer : IHttpContextRequestHeaderReplacer\n{\n    public Response Replace(HttpContext context, List<HeaderFindAndReplace> fAndRs)\n    {\n        foreach (var f in fAndRs)\n        {\n            if (context.Request.Headers.TryGetValue(f.Key, out var values))\n            {\n                var replaced = values[f.Index].Replace(f.Find, f.Replace);\n                context.Request.Headers.Remove(f.Key);\n                context.Request.Headers.Append(f.Key, replaced);\n            }\n        }\n\n        return new OkResponse();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/HttpResponseHeaderReplacer.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Headers;\n\npublic class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer\n{\n    private readonly IPlaceholders _placeholders;\n\n    public HttpResponseHeaderReplacer(IPlaceholders placeholders)\n    {\n        _placeholders = placeholders;\n    }\n\n    public Response Replace(HttpContext httpContext, List<HeaderFindAndReplace> fAndRs)\n    {\n        var response = httpContext.Items.DownstreamResponse();\n        var request = httpContext.Items.DownstreamRequest();\n\n        foreach (var f in fAndRs)\n        {\n            var dict = response.Headers.ToDictionary(x => x.Key);\n\n            //if the response headers contain a matching find and replace\n            if (dict.TryGetValue(f.Key, out var values))\n            {\n                //check to see if it is a placeholder in the find...\n                var placeholderValue = _placeholders.Get(f.Find, request);\n\n                if (!placeholderValue.IsError)\n                {\n                    //if it is we need to get the value of the placeholder\n                    var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash());\n\n                    response.Headers.Remove(response.Headers.First(item => item.Key == f.Key));\n                    response.Headers.Add(new Header(f.Key, new List<string> { replaced }));\n                }\n                else\n                {\n                    var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace);\n\n                    response.Headers.Remove(response.Headers.First(item => item.Key == f.Key));\n                    response.Headers.Add(new Header(f.Key, new List<string> { replaced }));\n                }\n            }\n        }\n\n        return new OkResponse();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/IAddHeadersToRequest.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Headers;\r\n\r\npublic interface IAddHeadersToRequest\r\n{\r\n    Response SetHeadersOnDownstreamRequest(List<ClaimToThing> claimsToThings, IEnumerable<System.Security.Claims.Claim> claims, DownstreamRequest downstreamRequest);\r\n\r\n    void SetHeadersOnDownstreamRequest(IEnumerable<AddHeader> headers, HttpContext context);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Headers/IAddHeadersToResponse.cs",
    "content": "using Ocelot.Configuration.Creator;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Headers;\n\npublic interface IAddHeadersToResponse\n{\n    void Add(List<AddHeader> addHeaders, DownstreamResponse response);\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Headers;\r\n\r\npublic interface IHttpContextRequestHeaderReplacer\r\n{\r\n    Response Replace(HttpContext context, List<HeaderFindAndReplace> fAndRs);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Headers;\n\npublic interface IHttpResponseHeaderReplacer\n{\n    public Response Replace(HttpContext httpContext, List<HeaderFindAndReplace> fAndRs);\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/IRemoveOutputHeaders.cs",
    "content": "﻿using Ocelot.Middleware;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Headers;\r\n\r\npublic interface IRemoveOutputHeaders\r\n{\r\n    Response Remove(List<Header> headers);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\r\nnamespace Ocelot.Headers.Middleware;\r\n\r\npublic class ClaimsToHeadersMiddleware : OcelotMiddleware\r\n{\r\n    private readonly RequestDelegate _next;\r\n    private readonly IAddHeadersToRequest _addHeadersToRequest;\r\n\r\n    public ClaimsToHeadersMiddleware(RequestDelegate next,\r\n        IOcelotLoggerFactory loggerFactory,\r\n        IAddHeadersToRequest addHeadersToRequest)\r\n            : base(loggerFactory.CreateLogger<ClaimsToHeadersMiddleware>())\r\n    {\r\n        _next = next;\r\n        _addHeadersToRequest = addHeadersToRequest;\r\n    }\r\n\r\n    public async Task Invoke(HttpContext httpContext)\r\n    {\r\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\r\n\r\n        if (downstreamRoute.ClaimsToHeaders.Any())\r\n        {\r\n            Logger.LogInformation(() => $\"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers\");\r\n\r\n            var downstreamRequest = httpContext.Items.DownstreamRequest();\r\n\r\n            var response = _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.ClaimsToHeaders, httpContext.User.Claims, downstreamRequest);\r\n\r\n            if (response.IsError)\r\n            {\r\n                Logger.LogWarning(\"Error setting headers on context, setting pipeline error\");\r\n\r\n                httpContext.Items.UpsertErrors(response.Errors);\r\n                return;\r\n            }\r\n\r\n            Logger.LogInformation(\"headers have been set on context\");\r\n        }\r\n\r\n        await _next.Invoke(httpContext);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Headers.Middleware;\n\npublic class HttpHeadersTransformationMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IHttpContextRequestHeaderReplacer _preReplacer;\n    private readonly IHttpResponseHeaderReplacer _postReplacer;\n    private readonly IAddHeadersToResponse _addHeadersToResponse;\n    private readonly IAddHeadersToRequest _addHeadersToRequest;\n\n    public HttpHeadersTransformationMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IHttpContextRequestHeaderReplacer preReplacer,\n        IHttpResponseHeaderReplacer postReplacer,\n        IAddHeadersToResponse addHeadersToResponse,\n        IAddHeadersToRequest addHeadersToRequest\n        )\n            : base(loggerFactory.CreateLogger<HttpHeadersTransformationMiddleware>())\n    {\n        _addHeadersToResponse = addHeadersToResponse;\n        _addHeadersToRequest = addHeadersToRequest;\n        _next = next;\n        _postReplacer = postReplacer;\n        _preReplacer = preReplacer;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n\n        var preFAndRs = downstreamRoute.UpstreamHeadersFindAndReplace;\n\n        //todo - this should be on httprequestmessage not httpcontext?\n        _preReplacer.Replace(httpContext, preFAndRs);\n\n        _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.AddHeadersToUpstream, httpContext);\n\n        await _next.Invoke(httpContext);\n\n        // todo check errors is ok\n        //todo put this check on the base class?\n        if (httpContext.Items.Errors().Count > 0)\n        {\n            return;\n        }\n\n        var postFAndRs = downstreamRoute.DownstreamHeadersFindAndReplace;\n\n        _postReplacer.Replace(httpContext, postFAndRs);\n\n        var downstreamResponse = httpContext.Items.DownstreamResponse();\n\n        _addHeadersToResponse.Add(downstreamRoute.AddHeadersToDownstream, downstreamResponse);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Headers/RemoveOutputHeaders.cs",
    "content": "using Ocelot.Middleware;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Headers;\r\n\r\npublic class RemoveOutputHeaders : IRemoveOutputHeaders\r\n{\r\n    /// <summary>\r\n    /// Some webservers return headers that cannot be forwarded to the client\r\n    /// in a given context such as transfer encoding chunked when ASP.NET is not\r\n    /// returning the response in this manner.\r\n    /// </summary>\r\n    private readonly string[] _unsupportedRequestHeaders =\n    {\r\n        \"Transfer-Encoding\",\r\n    };\r\n\r\n    public Response Remove(List<Header> headers)\r\n    {\r\n        headers.RemoveAll(x => _unsupportedRequestHeaders.Contains(x.Key));\r\n        return new OkResponse();\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.Infrastructure;\n\npublic class CannotAddPlaceholderError : Error\n{\n    public CannotAddPlaceholderError(string message)\n        : base(message, OcelotErrorCode.CannotAddPlaceholderError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs",
    "content": "using Ocelot.Errors;\r\n\r\nnamespace Ocelot.Infrastructure;\r\n\r\npublic class CannotRemovePlaceholderError : Error\r\n{\r\n    public CannotRemovePlaceholderError(string message)\r\n        : base(message, OcelotErrorCode.CannotRemovePlaceholderError, 404)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Claims/CannotFindClaimError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Infrastructure.Claims;\n\npublic class CannotFindClaimError : Error\n{\n    public CannotFindClaimError(string message)\n        : base(message, OcelotErrorCode.CannotFindClaimError, 403)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Claims/ClaimsParser.cs",
    "content": "﻿using Microsoft.Extensions.Primitives;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.Infrastructure.Claims;\n\npublic class ClaimsParser : IClaimsParser\n{\n    public Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index)\n    {\n        var claimResponse = GetValue(claims, key);\n\n        if (claimResponse.IsError)\n        {\n            return claimResponse;\n        }\n\n        if (string.IsNullOrEmpty(delimiter))\n        {\n            return claimResponse;\n        }\n\n        var splits = claimResponse.Data.Split(delimiter.ToCharArray());\n\n        if (splits.Length <= index || index < 0)\n        {\n            return new ErrorResponse<string>(new CannotFindClaimError($\"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}\"));\n        }\n\n        var value = splits[index];\n\n        return new OkResponse<string>(value);\n    }\n\n    public Response<List<string>> GetValuesByClaimType(IEnumerable<Claim> claims, string claimType)\n    {\n        var values = claims\n            .Where(x => x.Type == claimType) // Case sensitive or insensitive? That's the question!\n            .Select(x => x.Value)\n            .ToList();\n        return new OkResponse<List<string>>(values);\n    }\n\n    private static Response<string> GetValue(IEnumerable<Claim> claims, string key)\n    {\n        var claimValues = claims.Where(c => c.Type == key).Select(c => c.Value).ToArray();\n\n        if (claimValues.Length > 0)\n        {\n            return new OkResponse<string>(new StringValues(claimValues).ToString());\n        }\n\n        return new ErrorResponse<string>(new CannotFindClaimError($\"Cannot find claim for key: {key}\"));\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Claims/IClaimsParser.cs",
    "content": "﻿using Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.Infrastructure.Claims;\n\npublic interface IClaimsParser\n{\n    Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index);\n\n    Response<List<string>> GetValuesByClaimType(IEnumerable<Claim> claims, string claimType);\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Infrastructure;\n\n/// <summary>\n/// The configuration related implementation of the <see cref=\"IPlaceholders\"/> interface.\n/// </summary>\npublic partial class ConfigAwarePlaceholders : IPlaceholders\n{\n    private readonly IConfiguration _configuration;\n    private readonly IPlaceholders _placeholders;\n\n    public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders placeholders)\n    {\n        _configuration = configuration;\n        _placeholders = placeholders;\n    }\n\n    public Response<string> Get(string key)\n    {\n        var placeholderResponse = _placeholders.Get(key);\n\n        if (!placeholderResponse.IsError)\n        {\n            return placeholderResponse;\n        }\n\n        return GetFromConfig(CleanKey(key));\n    }\n\n    public Response<string> Get(string key, DownstreamRequest request)\n    {\n        var placeholderResponse = _placeholders.Get(key, request);\n\n        if (!placeholderResponse.IsError)\n        {\n            return placeholderResponse;\n        }\n\n        return GetFromConfig(CleanKey(key));\n    }\n\n    public Response Add(string key, Func<Response<string>> func)\n        => _placeholders.Add(key, func);\n\n    public Response Remove(string key)\n        => _placeholders.Remove(key);\n\n    [GeneratedRegex(@\"[{}]\", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\n    private static partial Regex Regex();\n\n    private static string CleanKey(string key)\n        => Regex().Replace(key, string.Empty);\n\n    private Response<string> GetFromConfig(string key)\n    {\n        var valueFromConfig = _configuration[key];\n        return valueFromConfig == null\n            ? new ErrorResponse<string>(new CouldNotFindPlaceholderError(key))\n            : new OkResponse<string>(valueFromConfig);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.Infrastructure;\n\npublic class CouldNotFindPlaceholderError : Error\n{\n    public CouldNotFindPlaceholderError(string placeholder)\n        : base($\"Unable to find placeholder called {placeholder}\", OcelotErrorCode.CouldNotFindPlaceholderError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/DelayedMessage.cs",
    "content": "namespace Ocelot.Infrastructure;\n\ninternal class DelayedMessage<T>\n{\n    public DelayedMessage(T message, int delay)\n    {\n        Delay = delay;\n        Message = message;\n    }\n\n    public T Message { get; set; }\n\n    public int Delay { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/DesignPatterns/Retry.cs",
    "content": "﻿using Ocelot.Logging;\n\nnamespace Ocelot.Infrastructure.DesignPatterns;\n\n/// <summary>\n/// Basic <seealso href=\"https://www.bing.com/search?q=Retry+pattern\">Retry pattern</seealso> for stabilizing integrated services.\n/// </summary>\n/// <remarks>Docs:\n/// <list type=\"bullet\">\n/// <item><see href=\"https://learn.microsoft.com/en-us/azure/architecture/patterns/retry\">Microsoft Learn | Retry pattern</see></item>\n/// </list>\n/// </remarks>\npublic static class Retry\n{\n    public const int DefaultRetryTimes = 3;\n    public const int DefaultWaitTimeMilliseconds = 25;\n\n    private static string GetMessage<T>(T operation, int retryNo, string message)\n        where T : Delegate\n        => $\"Ocelot {nameof(Retry)} strategy for the operation of '{operation.GetType()}' type -> {nameof(Retry)} No {retryNo}: {message}\";\n\n    /// <summary>\n    /// Retry a synchronous operation when an exception occurs or predicate is true, then delay and retry again.\n    /// </summary>\n    /// <typeparam name=\"TResult\">Type of the result of the sync operation.</typeparam>\n    /// <param name=\"operation\">Required Func-delegate of the operation.</param>\n    /// <param name=\"predicate\">Predicate to check, optionally.</param>\n    /// <param name=\"retryTimes\">Number of retries.</param>\n    /// <param name=\"waitTime\">Waiting time in milliseconds.</param>\n    /// <param name=\"logger\">Concrete logger from upper context.</param>\n    /// <returns>A <typeparamref name=\"TResult\"/> value as the result of the sync operation.</returns>\n    public static TResult Operation<TResult>(\n        Func<TResult> operation,\n        Predicate<TResult> predicate = null,\n        int retryTimes = DefaultRetryTimes, int waitTime = DefaultWaitTimeMilliseconds,\n        IOcelotLogger logger = null)\n    {\n        if (waitTime < 0)\n        {\n            waitTime = 0; // 0 means no thread sleeping\n        }\n\n        for (int n = 1; n < retryTimes; n++)\n        {\n            TResult result;\n            try\n            {\n                result = operation.Invoke();\n            }\n            catch (Exception e)\n            {\n                logger?.LogError(() => GetMessage(operation, n, $\"Caught exception of the {e.GetType()} type -> Message: {e.Message}.\"), e);\n                Thread.Sleep(waitTime);\n                continue; // the result is unknown, so continue to retry\n            }\n\n            // Apply predicate for known result\n            if (predicate?.Invoke(result) == true)\n            {\n                logger?.LogWarning(() => GetMessage(operation, n, $\"The predicate has identified erroneous state in the returned result. For further details, implement logging of the result's value or properties within the predicate method.\"));\n                Thread.Sleep(waitTime);\n                continue; // on erroneous state\n            }\n\n            // Happy path\n            return result;\n        }\n\n        // Last retry should generate native exception or other erroneous state(s)\n        logger?.LogDebug(() => GetMessage(operation, retryTimes, $\"Retrying lastly...\"));\n        return operation.Invoke(); // also final result must be analyzed in the upper context\n    }\n\n    /// <summary>\n    /// Retry an asynchronous operation when an exception occurs or predicate is true, then delay and retry again.\n    /// </summary>\n    /// <typeparam name=\"TResult\">Type of the result of the async operation.</typeparam>\n    /// <param name=\"operation\">Required Func-delegate of the operation.</param>\n    /// <param name=\"predicate\">Predicate to check, optionally.</param>\n    /// <param name=\"retryTimes\">Number of retries.</param>\n    /// <param name=\"waitTime\">Waiting time in milliseconds.</param>\n    /// <param name=\"logger\">Concrete logger from upper context.</param>\n    /// <returns>A <typeparamref name=\"TResult\"/> value as the result of the async operation.</returns>\n    public static async Task<TResult> OperationAsync<TResult>(\n        Func<Task<TResult>> operation, // required operation delegate\n        Predicate<TResult> predicate = null, // optional retry predicate for the result\n        int retryTimes = DefaultRetryTimes, int waitTime = DefaultWaitTimeMilliseconds, // retrying options\n        IOcelotLogger logger = null) // static injections\n    {\n        for (int n = 1; n < retryTimes; n++)\n        {\n            TResult result;\n            try\n            {\n                result = await operation?.Invoke();\n            }\n            catch (Exception e)\n            {\n                logger?.LogError(() => GetMessage(operation, n, $\"Caught exception of the {e.GetType()} type -> Message: {e.Message}.\"), e);\n                await (waitTime > 0 ? Task.Delay(waitTime) : Task.CompletedTask);\n                continue; // the result is unknown, so continue to retry\n            }\n\n            // Apply predicate for known result\n            if (predicate?.Invoke(result) == true)\n            {\n                logger?.LogWarning(() => GetMessage(operation, n, $\"The predicate has identified erroneous state in the returned result. For further details, implement logging of the result's value or properties within the predicate method.\"));\n                await (waitTime > 0 ? Task.Delay(waitTime) : Task.CompletedTask);\n                continue; // on erroneous state\n            }\n\n            // Happy path\n            return result;\n        }\n\n        // Last retry should generate native exception or other erroneous state(s)\n        logger?.LogDebug(() => GetMessage(operation, retryTimes, $\"Retrying lastly...\"));\n        return await operation?.Invoke(); // also final result must be analyzed in the upper context\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs",
    "content": "﻿using Ocelot.Errors;\n\r\nnamespace Ocelot.Infrastructure.Extensions;\r\n\r\npublic static class ErrorListExtensions\r\n{\r\n    private static readonly string Nl = Environment.NewLine;\r\n    private static readonly string Em = string.Empty;\r\n\r\n    /// <summary>\r\n    /// Joins all errors using <see cref=\"Environment.NewLine\"/> separator, in the format \"Code: Message\".\r\n    /// </summary>\r\n    /// <param name=\"errors\">The list of errors to extend.</param>\r\n    /// <param name=\"before\">Flag to insert new line before.</param>\r\n    /// <param name=\"after\">Flag to insert new line after.</param>\r\n    /// <returns>Single <see cref=\"string\"/> with all errors.</returns>\r\n    public static string ToErrorString(this List<Error> errors, bool before = false, bool after = false)\r\n        => (before ? Nl : Em) + string.Join(Nl, errors.Select(e => e.ToString())) + (after ? Nl : Em);\n}\r\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/HttpContextExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.Infrastructure.Extensions;\n\npublic static class HttpContextExtensions\n{\n    public static bool IsOptionsMethod(this HttpContext context)\n        => context.Request.IsOptionsMethod();\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/HttpRequestExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.Infrastructure.Extensions;\n\npublic static class HttpRequestExtensions\n{\n    public static bool IsOptionsMethod(this HttpRequest request)\n        => HttpMethod.Options.Method.Equals(request.Method, StringComparison.OrdinalIgnoreCase);\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/IEnumerableExtensions.cs",
    "content": "﻿namespace Ocelot.Infrastructure.Extensions;\n\npublic static class IEnumerableExtensions\n{\n    /// <summary>\n    /// Converts a collection of <see cref=\"string\"/> representations of HTTP methods (verbs) into a hashed set of <see cref=\"HttpMethod\"/> objects.\n    /// </summary>\n    /// <remarks>Note:\n    /// <list type=\"number\">\n    /// <item>Trims each string in the collection.</item>\n    /// <item>Does not throw <see cref=\"ArgumentNullException\"/> if the collection is <see langword=\"null\"/>.</item>\n    /// </list>\n    /// </remarks>\n    /// <param name=\"collection\">The collection of HTTP method strings.</param>\n    /// <returns>A <see cref=\"HashSet{HttpMethod}\"/> object, where T is <see cref=\"HttpMethod\"/>.</returns>\n    public static HashSet<HttpMethod> ToHttpMethods(this IEnumerable<string> collection)\n    {\n        collection ??= Enumerable.Empty<string>();\n        return collection.Select(verb => new HttpMethod(verb.Trim())).ToHashSet();\n    }\n\n    /// <summary>\n    /// Helper function to convert multiple objects as strings into a comma-separated string aka CSV.\n    /// </summary>\n    /// <typeparam name=\"T\">The type of the objects.</typeparam>\n    /// <param name=\"values\">The collection of <typeparamref name=\"T\"/> to join by comma separator.</param>\n    /// <returns>A <see langword=\"string\"/> in the comma-separated format.</returns>\n    public static string Csv<T>(this IEnumerable<T> values)\n        => string.Join(',', values.NotNull());\n\n    public static IEnumerable<T> NotNull<T>(this IEnumerable<T> collection)\n        => collection ?? Enumerable.Empty<T>();\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/Int32Extensions.cs",
    "content": "﻿namespace Ocelot.Infrastructure.Extensions;\n\npublic static class Int32Extensions\n{\n    public static int Ensure(this int value, int low = 0)\n        => value < low ? low : value;\n\n    public static int Positive(this int value)\n        => Ensure(value, 1);\n\n    /// <summary>\n    /// Ensures nullable integer is positive, otherwise converts the value to default one.\n    /// </summary>\n    /// <param name=\"value\">The value.</param>\n    /// <param name=\"toDefault\">Default integer to convert to.</param>\n    /// <returns>A nullable <see cref=\"int\"/> value.</returns>\n    public static int? Positive(this int? value, int toDefault = 1)\n        => !value.HasValue ? null :\n            value.Value > 0 ? value.Value : toDefault;\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/StringBuilderExtensions.cs",
    "content": "﻿namespace Ocelot.Infrastructure.Extensions;\n\npublic static class StringBuilderExtensions\n{\n\n    /// <summary>Helper method to add a string to the key builder, using a comma as the default separator.</summary>\n    /// <param name=\"builder\">The key builder instance.</param>\n    /// <param name=\"next\">The next string to append.</param>\n    /// <param name=\"separator\">The character used to separate entries.</param>\n    /// <returns>Returns the same builder instance.</returns>\n    public static StringBuilder AppendNext(this StringBuilder builder, string next, char separator = ',')\n    {\n        if (builder.Length > 0)\n        {\n            builder.Append(separator);\n        }\n\n        return builder.Append(next);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Extensions/StringExtensions.cs",
    "content": "﻿namespace Ocelot.Infrastructure.Extensions;\r\n\r\npublic static class StringExtensions\r\n{\r\n    /// <summary>Indicates whether a specified string is <see langword=\"null\"/>, empty, or consists only of white-space characters.</summary>\r\n    /// <remarks>This is shortcut for the <see cref=\"string.IsNullOrWhiteSpace(string?)\"/> method.</remarks>\r\n    /// <param name=\"str\">The string to test.</param>\r\n    /// <returns><see langword=\"true\"/> if the <paramref name=\"str\"/> parameter is <see langword=\"null\" /> or <see cref=\"string.Empty\"/>, or if <paramref name=\"str\"/> consists exclusively of white-space characters.</returns>\r\n    public static bool IsEmpty(this string str) => string.IsNullOrWhiteSpace(str);\r\n    public static bool IsNotEmpty(this string str) => !string.IsNullOrWhiteSpace(str);\r\n\r\n    /// <summary>Defaults to the default string if the current string is null or empty.</summary>\r\n    /// <remarks>Based on the <see cref=\"string.IsNullOrWhiteSpace(string?)\"/> method.</remarks>\r\n    /// <param name=\"str\">The current string.</param>\r\n    /// <param name=\"def\">The default string.</param>\r\n    /// <returns>The <paramref name=\"def\"/> string if <paramref name=\"str\"/> is empty; otherwise, the <paramref name=\"str\"/> string.</returns>\r\n    public static string IfEmpty(this string str, string def) => string.IsNullOrWhiteSpace(str) ? def : str;\r\n\r\n    /// <summary>Removes the prefix from the beginning of the string repeatedly until all occurrences are eliminated.</summary>\r\n    /// <param name=\"source\">The string to trim.</param>\r\n    /// <param name=\"prefix\">The prefix string to remove.</param>\r\n    /// <param name=\"comparison\">The 2nd argument of the <see cref=\"string.StartsWith(string)\"/> method.</param>\r\n    /// <returns>A new <see cref=\"string\"/> without the prefix all occurrences.</returns>\r\n    public static string TrimPrefix(this string source, string prefix, StringComparison comparison = StringComparison.Ordinal)\r\n    {\r\n        if (source == null || string.IsNullOrEmpty(prefix))\r\n        {\r\n            return source;\r\n        }\r\n\r\n        var s = source;\r\n        while (s.StartsWith(prefix, comparison))\r\n        {\r\n            s = s[prefix.Length..];\r\n        }\r\n\r\n        return s;\r\n    }\r\n\r\n    public const char Slash = '/';\r\n\r\n    /// <summary>Ensures that the last char of the string is forward slash, '/'.</summary>\r\n    /// <param name=\"source\">The string to check its last slash char.</param>\r\n    /// <returns>A <see cref=\"string\"/> witl the last forward slash.</returns>\r\n    public static string LastCharAsForwardSlash(this string source)\r\n        => source.EndsWith(Slash) ? source : source + Slash;\r\n\r\n    public static string Plural(this int count) => count == 1 ? string.Empty : \"s\";\r\n    public static string Plural(this string source, int count) => count == 1 ? source : string.Concat(source, \"s\");\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/FrameworkDescription.cs",
    "content": "using System.Runtime.InteropServices;\n\nnamespace Ocelot.Infrastructure;\n\npublic class FrameworkDescription : IFrameworkDescription\n{\n    public string Get()\n    {\n        return RuntimeInformation.FrameworkDescription;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/IBus.cs",
    "content": "namespace Ocelot.Infrastructure;\n\npublic interface IBus<T>\n{\n    void Subscribe(Action<T> action);\n\n    void Publish(T message, int delay);\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/IFrameworkDescription.cs",
    "content": "namespace Ocelot.Infrastructure;\n\npublic interface IFrameworkDescription\n{\n    string Get();\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/IPlaceholders.cs",
    "content": "using Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Infrastructure;\n\npublic interface IPlaceholders\n{\n    Response<string> Get(string key);\n\n    Response<string> Get(string key, DownstreamRequest request);\n\n    Response Add(string key, Func<Response<string>> func);\n\n    Response Remove(string key);\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/InMemoryBus.cs",
    "content": "namespace Ocelot.Infrastructure;\n\npublic class InMemoryBus<T> : IBus<T>\n{\n    private readonly BlockingCollection<DelayedMessage<T>> _queue;\n    private readonly List<Action<T>> _subscriptions;\n    private readonly Thread _processing;\n\n    public InMemoryBus()\n    {\n        _queue = new BlockingCollection<DelayedMessage<T>>();\n        _subscriptions = new List<Action<T>>();\n        _processing = new Thread(async () => await Process());\n        _processing.Start();\n    }\n\n    public void Subscribe(Action<T> action)\n    {\n        _subscriptions.Add(action);\n    }\n\n    public void Publish(T message, int delay)\n    {\n        var delayed = new DelayedMessage<T>(message, delay);\n        _queue.Add(delayed);\n    }\n\n    private async Task Process()\n    {\n        foreach (var delayedMessage in _queue.GetConsumingEnumerable())\n        {\n            await Task.Delay(delayedMessage.Delay);\n\n            foreach (var subscription in _subscriptions)\n            {\n                subscription(delayedMessage.Message);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/Placeholders.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing System.Net.Sockets;\n\nnamespace Ocelot.Infrastructure;\n\npublic class Placeholders : IPlaceholders\n{\n    public const char OpeningBrace = '{';\n    public const char ClosingBrace = '}';\n\n    private readonly Dictionary<string, Func<Response<string>>> _placeholders;\n    private readonly Dictionary<string, Func<DownstreamRequest, string>> _requestPlaceholders;\n    private readonly IBaseUrlFinder _finder;\n    private readonly IRequestScopedDataRepository _repo;\n    private readonly IHttpContextAccessor _contextAccessor;\n\n    public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo, IHttpContextAccessor contextAccessor)\n    {\n        _repo = repo;\n        _contextAccessor = contextAccessor;\n        _finder = finder;\n        _placeholders = new Dictionary<string, Func<Response<string>>>\n        {\n            { \"{BaseUrl}\", GetBaseUrl },\n            { \"{TraceId}\", GetTraceId },\n            { \"{RemoteIpAddress}\", GetRemoteIpAddress },\n            { \"{UpstreamHost}\", GetUpstreamHost },\n        };\n\n        _requestPlaceholders = new Dictionary<string, Func<DownstreamRequest, string>>\n        {\n            { \"{DownstreamBaseUrl}\", GetDownstreamBaseUrl },\n        };\n    }\n\n    public Response<string> Get(string key)\n    {\n        if (_placeholders.TryGetValue(key, out Func<Response<string>> valueFunc))\n        {\n            var response = valueFunc.Invoke();\n            if (!response.IsError)\n            {\n                return new OkResponse<string>(response.Data);\n            }\n        }\n\n        return new ErrorResponse<string>(new CouldNotFindPlaceholderError(key));\n    }\n\n    public Response<string> Get(string key, DownstreamRequest request)\n    {\n        return _requestPlaceholders.TryGetValue(key, out var func)\n            ? new OkResponse<string>(func.Invoke(request))\n            : new ErrorResponse<string>(new CouldNotFindPlaceholderError(key));\n    }\n\n    public Response Add(string key, Func<Response<string>> func)\n    {\n        return _placeholders.TryAdd(key, func)\n            ? new OkResponse()\n            : new ErrorResponse(new CannotAddPlaceholderError($\"Unable to add placeholder: {key}, placeholder already exists\"));\n    }\n\n    public Response Remove(string key)\n    {\n        if (!_placeholders.ContainsKey(key))\n        {\n            return new ErrorResponse(new CannotRemovePlaceholderError($\"Unable to remove placeholder: {key}, placeholder does not exists\"));\n        }\n\n        _placeholders.Remove(key);\n        return new OkResponse();\n    }\n\n    private Response<string> GetRemoteIpAddress()\n    {\n        // this can blow up so adding try catch and return error\n        try\n        {\n            var ip = _contextAccessor.HttpContext.Connection.RemoteIpAddress\n                ?? Dns.GetHostAddresses(string.Empty).FirstOrDefault(a => a.AddressFamily != AddressFamily.InterNetworkV6); // detect localhost network interface, a lifehack\n            return ip != null\n                ? new OkResponse<string>(ip.ToString())\n                : new ErrorResponse<string>(new CouldNotFindPlaceholderError(\"{RemoteIpAddress}\"));\n        }\n        catch\n        {\n            return new ErrorResponse<string>(new CouldNotFindPlaceholderError(\"{RemoteIpAddress}\"));\n        }\n    }\n\n    private static string GetDownstreamBaseUrl(DownstreamRequest x)\n    {\n        var downstreamUrl = $\"{x.Scheme}://{x.Host}\";\n        if (x.Port != 80 && x.Port != 443)\n        {\n            downstreamUrl = $\"{downstreamUrl}:{x.Port}\";\n        }\n\n        return $\"{downstreamUrl}/\";\n    }\n\n    private Response<string> GetTraceId()\n    {\n        var traceId = _repo.Get<string>(OcelotHttpTracingHandler.TraceId);\n        return traceId.IsError\n            ? new ErrorResponse<string>(traceId.Errors)\n            : new OkResponse<string>(traceId.Data);\n    }\n\n    private Response<string> GetBaseUrl() => new OkResponse<string>(_finder.Find());\n\n    private Response<string> GetUpstreamHost()\n    {\n        try\n        {\n            return _contextAccessor.HttpContext.Request.Headers.TryGetValue(\"Host\", out var upstreamHost)\n                ? new OkResponse<string>(upstreamHost.First())\n                : new ErrorResponse<string>(new CouldNotFindPlaceholderError(\"{UpstreamHost}\"));\n        }\n        catch\n        {\n            return new ErrorResponse<string>(new CouldNotFindPlaceholderError(\"{UpstreamHost}\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/RegexGlobal.cs",
    "content": "﻿using Ocelot.DependencyInjection;\n\nnamespace Ocelot.Infrastructure;\n\npublic static class RegexGlobal\n{\n    static RegexGlobal()\n    {\n        RegexCacheSize = DefaultRegexCacheSize;\n        DefaultMatchTimeout = TimeSpan.FromMilliseconds(DefaultMatchTimeoutMilliseconds);\n        AppDomain.CurrentDomain.SetData(\"REGEX_DEFAULT_MATCH_TIMEOUT\", DefaultMatchTimeout);\n    }\n\n    /// <summary>Default value of the <see cref=\"RegexCacheSize\"/> property.</summary>\n    public const int DefaultRegexCacheSize = 100;\n\n    /// <summary>Gets or sets the global value to assign to the <see cref=\"Regex.CacheSize\"/> property.\n    /// <para>Ocelot forcibly assigns this value during app startup, see <see cref=\"ConfigurationBuilderExtensions\"/> class.</para>\n    /// </summary>\n    /// <remarks>Default value is <c>100</c> aka <see cref=\"DefaultRegexCacheSize\"/>.<br/>\n    /// Default .NET value of <see cref=\"Regex.CacheSize\"/> is <c>15</c>.</remarks>\n    /// <value>An <see cref=\"int\"/> value.</value>\n    public static int RegexCacheSize { get; set; }\n\n#pragma warning disable IDE0079 // Remove unnecessary suppression\n#pragma warning disable CS1574 // File name must match first type name\n    /// <summary>Default value for the <see cref=\"GeneratedRegexAttribute\"/> and the <see cref=\"Regex\"/> constructors.</summary>\n#pragma warning restore CS1574 // File name must match first type name\n#pragma warning restore IDE0079 // Remove unnecessary suppression\n    public const int DefaultMatchTimeoutMilliseconds = 100;\n\n    /// <summary>Default match timeout for the <see cref=\"Regex\"/> constructors.</summary>\n    /// <remarks>Default value is <c>100</c> ms aka <see cref=\"DefaultMatchTimeoutMilliseconds\"/>.</remarks>\n    /// <value>A <see cref=\"TimeSpan\"/> value.</value>\n    public static TimeSpan DefaultMatchTimeout { get; set; }\n\n    public static Regex New(string pattern)\n        => new(pattern, RegexOptions.Compiled, DefaultMatchTimeout);\n    public static Regex New(string pattern, RegexOptions options)\n        => new(pattern, options | RegexOptions.Compiled, DefaultMatchTimeout);\n    public static Regex New(string pattern, RegexOptions options, TimeSpan matchTimeout)\n        => new(pattern, options | RegexOptions.Compiled, matchTimeout);\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Infrastructure.RequestData;\n\npublic class CannotAddDataError : Error\n{\n    public CannotAddDataError(string message) : base(message, OcelotErrorCode.CannotAddDataError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Infrastructure.RequestData;\n\npublic class CannotFindDataError : Error\n{\n    public CannotFindDataError(string message) : base(message, OcelotErrorCode.CannotFindDataError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Infrastructure.RequestData;\r\n\r\npublic class HttpDataRepository : IRequestScopedDataRepository\r\n{\r\n    private readonly IHttpContextAccessor _contextAccessor;\r\n\r\n    public HttpDataRepository(IHttpContextAccessor contextAccessor)\r\n    {\r\n        _contextAccessor = contextAccessor;\r\n    }\r\n\r\n    public Response Add<T>(string key, T value)\r\n    {\r\n        try\r\n        {\r\n            _contextAccessor.HttpContext.Items.Add(key, value);\r\n            return new OkResponse();\r\n        }\r\n        catch (Exception exception)\r\n        {\r\n            return new ErrorResponse(new CannotAddDataError(string.Format($\"Unable to add data for key: {key}, exception: {exception.Message}\")));\r\n        }\r\n    }\r\n\r\n    public Response Update<T>(string key, T value)\r\n    {\r\n        try\r\n        {\r\n            _contextAccessor.HttpContext.Items[key] = value;\r\n            return new OkResponse();\r\n        }\r\n        catch (Exception exception)\r\n        {\r\n            return new ErrorResponse(new CannotAddDataError(string.Format($\"Unable to update data for key: {key}, exception: {exception.Message}\")));\r\n        }\r\n    }\r\n\r\n    public Response<T> Get<T>(string key)\r\n    {\r\n        if (_contextAccessor?.HttpContext?.Items == null)\r\n        {\r\n            return new ErrorResponse<T>(new CannotFindDataError($\"Unable to find data for key: {key} because HttpContext or HttpContext.Items is null\"));\r\n        }\r\n\r\n        return _contextAccessor.HttpContext.Items.TryGetValue(key, out var item)\r\n            ? new OkResponse<T>((T)item)\r\n            : new ErrorResponse<T>(new CannotFindDataError($\"Unable to find data for key: {key}\"));\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs",
    "content": "using Ocelot.Responses;\r\n\r\nnamespace Ocelot.Infrastructure.RequestData;\r\n\r\npublic interface IRequestScopedDataRepository\r\n{\r\n    Response Add<T>(string key, T value);\r\n\r\n    Response Update<T>(string key, T value);\r\n\r\n    Response<T> Get<T>(string key);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Balancers/CookieStickySessions.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer.Balancers;\n\npublic class CookieStickySessions : ILoadBalancer\n{\n    /// <summary>\n    /// Track default ASP.NET Core session idle timeout here: <see href=\"https://github.com/dotnet/aspnetcore/blob/0585ae7d9f9361bed031ce01e4a2f6eeab7438c4/src/Middleware/Session/src/SessionOptions.cs#L38\">SessionOptions.IdleTimeout</see>.\n    /// </summary>\n    public const int DefSessionExpiryMinutes = 20;\n    public static readonly int DefSessionExpiryMilliseconds;\n\n    /// <summary>\n    /// Track default ASP.NET Core session cookie name here: <see href=\"https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Session/src/SessionDefaults.cs#L14\">SessionDefaults.CookieName</see>.\n    /// </summary>\n    public static readonly string DefSessionCookieName = Microsoft.AspNetCore.Session.SessionDefaults.CookieName;\n\n    static CookieStickySessions()\n    {\n#if NET9_0_OR_GREATER\n        DefSessionExpiryMilliseconds = DefSessionExpiryMinutes * (int)TimeSpan.MillisecondsPerMinute;\n#else\n        // TODO Migrate to TimeSpan.MillisecondsPerMinute after net8.0 deprecation\n        DefSessionExpiryMilliseconds = (int)TimeSpan.FromMinutes(DefSessionExpiryMinutes).TotalMilliseconds;\n#endif\n    }\n\n    private readonly int _keyExpiryInMs;\n    private readonly string _cookieName;\n    private readonly ILoadBalancer _loadBalancer;\n    private readonly IBus<StickySession> _bus;\n\n#if NET9_0_OR_GREATER\n    private static readonly Lock Locker = new();\n#else\n    private static readonly object Locker = new();\n#endif\n    private static readonly Dictionary<string, StickySession> Stored = new(); // TODO Inject instead of static sharing\n\n    public string Type => nameof(CookieStickySessions);\n\n    public CookieStickySessions(ILoadBalancer loadBalancer, string cookieName, int keyExpiryInMs, IBus<StickySession> bus)\n    {\n        _bus = bus;\n        _cookieName = cookieName;\n        _keyExpiryInMs = keyExpiryInMs;\n        _loadBalancer = loadBalancer;\n        _bus.Subscribe(CheckExpiry);\n    }\n\n    private void CheckExpiry(StickySession sticky)\n    {\n        // TODO Get test coverage for this\n        lock (Locker)\n        {\n            if (!Stored.TryGetValue(sticky.Key, out var session) || session.Expiry >= DateTime.UtcNow)\n            {\n                return;\n            }\n\n            Stored.Remove(session.Key);\n            _loadBalancer.Release(session.HostAndPort);\n        }\n    }\n\n    public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n    {\n        var route = httpContext.Items.DownstreamRoute();\n        var serviceName = route.LoadBalancerKey;\n        var cookie = httpContext.Request.Cookies[_cookieName];\n        var key = $\"{serviceName}:{cookie}\"; // strong key name because of static store\n        lock (Locker)\n        {\n            if (Stored.TryGetValue(key, out StickySession cached))\n            {\n                var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key);\n                Update(key, updated);\n                return Task.FromResult<Response<ServiceHostAndPort>>(new OkResponse<ServiceHostAndPort>(updated.HostAndPort));\n            }\n\n            // There is no value in the store, so lease it now!\n            var next = _loadBalancer.LeaseAsync(httpContext).GetAwaiter().GetResult(); // unfortunately the operation must be synchronous\n            if (next.IsError)\n            {\n                return Task.FromResult<Response<ServiceHostAndPort>>(new ErrorResponse<ServiceHostAndPort>(next.Errors));\n            }\n\n            var ss = new StickySession(next.Data, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key);\n            Update(key, ss);\n            return Task.FromResult<Response<ServiceHostAndPort>>(new OkResponse<ServiceHostAndPort>(next.Data));\n        }\n    }\n\n    protected void Update(string key, StickySession value)\n    {\n        lock (Locker)\n        {\n            Stored[key] = value;\n            _bus.Publish(value, _keyExpiryInMs);\n        }\n    }\n\n    public void Release(ServiceHostAndPort hostAndPort)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Balancers/LeastConnection.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer.Balancers;\n\npublic class LeastConnection : ILoadBalancer\n{\n    private readonly Func<Task<List<Service>>> _services;\n    private readonly List<Lease> _leases;\n    private readonly string _serviceName;\n#if NET9_0_OR_GREATER\n    private static readonly Lock SyncRoot = new();\n#else\n    private static readonly object SyncRoot = new();\n#endif\n\n    public string Type => nameof(LeastConnection);\n\n    public LeastConnection(Func<Task<List<Service>>> services, string serviceName)\n    {\n        _services = services;\n        _serviceName = serviceName;\n        _leases = new List<Lease>();\n    }\n\n    public event EventHandler<LeaseEventArgs> Leased;\n    protected virtual void OnLeased(LeaseEventArgs e) => Leased?.Invoke(this, e);\n\n    public async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n    {\n        var services = await _services.Invoke();\n        if ((services?.Count ?? 0) == 0)\n        {\n            return new ErrorResponse<ServiceHostAndPort>(new ServicesAreNullError($\"Services were null/empty in {Type} for '{_serviceName}' during {nameof(LeaseAsync)} operation!\"));\n        }\n\n        lock (SyncRoot)\n        {\n            //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something?\n            UpdateLeasing(services);\n\n            Lease wanted = GetLeaseWithLeastConnections();\n            _ = Update(ref wanted, true);\n\n            var index = services.FindIndex(s => s.HostAndPort == wanted);\n            OnLeased(new(wanted, services[index], index));\n\n            return new OkResponse<ServiceHostAndPort>(new(wanted.HostAndPort));\n        }\n    }\n\n    public void Release(ServiceHostAndPort hostAndPort)\n    {\n        lock (SyncRoot)\n        {\n            var matchingLease = _leases.Find(l => l == hostAndPort);\n            if (matchingLease != Lease.Null)\n            {\n                _ = Update(ref matchingLease, false);\n            }\n        }\n    }\n\n    private int Update(ref Lease item, bool increase)\n    {\n        var index = _leases.IndexOf(item);\n        _ = increase ? item.Connections++ : item.Connections--;\n        _leases[index] = item; // write the value back to the position\n        return index;\n    }\n\n    private Lease GetLeaseWithLeastConnections()\n    {\n        var min = _leases.Min(l => l.Connections);\n        return _leases.Find(l => l.Connections == min);\n    }\n\n    private void UpdateLeasing(List<Service> services)\n    {\n        if (_leases.Count > 0)\n        {\n            _leases.RemoveAll(l => !services.Exists(s => s.HostAndPort == l));\n\n            services.Where(s => !_leases.Exists(l => l == s.HostAndPort))\n                .ToList()\n                .ForEach(s => _leases.Add(new(s.HostAndPort, 0)));\n        }\n        else\n        {\n            services.ForEach(s => _leases.Add(new(s.HostAndPort)));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Balancers/NoLoadBalancer.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer.Balancers;\n\npublic class NoLoadBalancer : ILoadBalancer\n{\n    private readonly Func<Task<List<Service>>> _services;\n\n    public NoLoadBalancer(Func<Task<List<Service>>> services)\n    {\n        _services = services;\n    }\n\n    public string Type => nameof(NoLoadBalancer);\n\n    public async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n    {\n        var services = await _services();\n\n        if (services == null || services.Count == 0)\n        {\n            return new ErrorResponse<ServiceHostAndPort>(new ServicesAreEmptyError($\"There were no services in {Type}!\"));\n        }\n\n        var service = await Task.FromResult(services.FirstOrDefault());\n        return new OkResponse<ServiceHostAndPort>(service.HostAndPort);\n    }\n\n    public void Release(ServiceHostAndPort hostAndPort)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Balancers/RoundRobin.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer.Balancers;\n\npublic class RoundRobin : ILoadBalancer\n{\n    private readonly Func<Task<List<Service>>> _servicesDelegate;\n    private readonly string _serviceName;\n    private readonly List<Lease> _leasing;\n\n    public string Type => nameof(RoundRobin);\n\n    public RoundRobin(Func<Task<List<Service>>> services, string serviceName)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n        _servicesDelegate = services;\n        _serviceName = serviceName;\n        _leasing = new();\n    }\n\n    private static readonly Dictionary<string, int> LastIndices = new();\n#if NET9_0_OR_GREATER\n    private static readonly Lock SyncRoot = new();\n#else\n    private static readonly object SyncRoot = new();\n#endif\n\n    public event EventHandler<LeaseEventArgs> Leased;\n    protected virtual void OnLeased(LeaseEventArgs e) => Leased?.Invoke(this, e);\n\n    public virtual async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n    {\n        var services = await _servicesDelegate.Invoke() ?? new(0);\n        if (services.Count == 0)\n        {\n            return new ErrorResponse<ServiceHostAndPort>(new ServicesAreEmptyError($\"There were no services in {Type} for '{_serviceName}' during {nameof(LeaseAsync)} operation!\"));\n        }\n\n        lock (SyncRoot)\n        {\n            var readMe = CaptureState(services, out int count);\n            if (!TryScanNext(readMe, out Service next, out int index))\n            {\n                return new ErrorResponse<ServiceHostAndPort>(new ServicesAreNullError($\"The service at index {index} was null in {Type} for {_serviceName} during the {nameof(LeaseAsync)} operation. Total services count: {count}.\"));\n            }\n\n            ProcessLeasing(readMe, next, index); // Happy path: Lease now\n            return new OkResponse<ServiceHostAndPort>(next.HostAndPort);\n        }\n    }\n\n    public virtual void Release(ServiceHostAndPort hostAndPort) { }\n\n    /// <summary>Capture the count value because another thread might modify the list.</summary>\n    /// <param name=\"services\">Mutable collection of services.</param>\n    /// <param name=\"count\">Captured count value.</param>\n    /// <returns>Captured collection as a <see cref=\"Array\"/> object.</returns>\n    private static Service[] CaptureState(List<Service> services, out int count)\n    {\n        // Capture the count value because another thread might modify the list\n        count = services.Count;\n        var readMe = new Service[count];\n        services.CopyTo(readMe);\n        return readMe;\n    }\n\n    /// <summary>Scan for the next online service instance which must be healthy.</summary>\n    /// <param name=\"readme\">Read-only collection.</param>\n    /// <param name=\"next\">The next online service to return.</param>\n    /// <param name=\"index\">The index of the next service to return.</param>\n    /// <returns><see langword=\"true\"/> if found next online service; otherwise <see langword=\"false\"/>.</returns>\n    private bool TryScanNext(Service[] readme, out Service next, out int index)\n    {\n        int length = readme.Length, stop = length;\n        LastIndices.TryGetValue(_serviceName, out int last);\n        if (last >= length)\n        {\n            last = 0;\n        }\n\n        next = null;\n        index = last;\n\n        // Scan for the next service instance\n        // TODO Check real health status\n        while (next?.HostAndPort == null && stop-- > 0)\n        {\n            index = last;\n            next = readme[last];\n            LastIndices[_serviceName] = (++last < length) ? last : 0;\n        }\n\n        return next != null;\n    }\n\n    private void ProcessLeasing(Service[] readme, Service next, int index)\n    {\n        UpdateLeasing(readme);\n        Lease wanted = GetLease(next);\n        _ = Update(ref wanted, true); // perform counting based on Connections\n        OnLeased(new(wanted, next, index));\n    }\n\n    private int Update(ref Lease item, bool increase)\n    {\n        var index = _leasing.IndexOf(item);\n        _ = increase ? item.Connections++ : item.Connections--;\n        _leasing[index] = item; // write the value back to the position\n        return index;\n    }\n\n    private Lease GetLease(Service @for) => _leasing.Find(l => l == @for.HostAndPort);\n\n    private void UpdateLeasing(IList<Service> services)\n    {\n        // Don't remove leasing data of old services, so keep data during life time of the load balancer\n        // _leasing.RemoveAll(l => services.All(s => s?.HostAndPort != l));\n        var newLeases = services\n            .Where(s => s != null && !_leasing.Exists(l => l == s.HostAndPort))\n            .Select(s => new Lease(s.HostAndPort))\n            .ToArray(); // capture leasing state and produce new collection\n        _leasing.AddRange(newLeases);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Creators/CookieStickySessionsCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Infrastructure;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Creators;\n\npublic class CookieStickySessionsCreator : ILoadBalancerCreator\n{\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        var options = route.LoadBalancerOptions;\n        var loadBalancer = new RoundRobin(serviceProvider.GetAsync, route.LoadBalancerKey);\n        var bus = new InMemoryBus<StickySession>();\n        return new OkResponse<ILoadBalancer>(\n            new CookieStickySessions(loadBalancer, options.Key, options.ExpiryInMs, bus));\n    }\n\n    public string Type => nameof(CookieStickySessions);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Creators/DelegateInvokingLoadBalancerCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Creators;\n\npublic class DelegateInvokingLoadBalancerCreator<T> : ILoadBalancerCreator\n    where T : ILoadBalancer\n{\n    private readonly Func<DownstreamRoute, IServiceDiscoveryProvider, ILoadBalancer> _creatorFunc;\n\n    public DelegateInvokingLoadBalancerCreator(\n        Func<DownstreamRoute, IServiceDiscoveryProvider, ILoadBalancer> creatorFunc)\n    {\n        _creatorFunc = creatorFunc;\n    }\n\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        try\n        {\n            return new OkResponse<ILoadBalancer>(_creatorFunc(route, serviceProvider));\n        }\n        catch (Exception e)\n        {\n            return new ErrorResponse<ILoadBalancer>(new InvokingLoadBalancerCreatorError(e));\n        }\n    }\n\n    public string Type => typeof(T).Name;\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Creators/LeastConnectionCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Creators;\n\npublic class LeastConnectionCreator : ILoadBalancerCreator\n{\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        var loadBalancer = new LeastConnection(\n            serviceProvider.GetAsync,\n            !string.IsNullOrEmpty(route.ServiceName)\n                ? route.ServiceName\n                : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key\n        return new OkResponse<ILoadBalancer>(loadBalancer);\n    }\n\n    public string Type => nameof(LeastConnection);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Creators/NoLoadBalancerCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Creators;\n\npublic class NoLoadBalancerCreator : ILoadBalancerCreator\n{\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        return new OkResponse<ILoadBalancer>(new NoLoadBalancer(async () => await serviceProvider.GetAsync()));\n    }\n\n    public string Type => nameof(NoLoadBalancer);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Creators/RoundRobinCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Creators;\n\npublic class RoundRobinCreator : ILoadBalancerCreator\n{\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        var loadBalancer = new RoundRobin(\n            serviceProvider.GetAsync,\n            !string.IsNullOrEmpty(route.ServiceName)\n                ? route.ServiceName\n                : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key\n        return new OkResponse<ILoadBalancer>(loadBalancer);\n    }\n\n    public string Type => nameof(RoundRobin);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Errors/CouldNotFindLoadBalancerCreatorError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.LoadBalancer.Errors;\n\npublic class CouldNotFindLoadBalancerCreatorError : Error\n{\n    public CouldNotFindLoadBalancerCreatorError(string message)\n        : base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator, StatusCodes.Status404NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Errors/InvokingLoadBalancerCreatorError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.LoadBalancer.Errors;\n\npublic class InvokingLoadBalancerCreatorError : Error\n{\n    public InvokingLoadBalancerCreatorError(Exception e)\n        : base($\"Error when invoking user provided load balancer creator function, Message: {e.Message}, StackTrace: {e.StackTrace}\",\n            OcelotErrorCode.ErrorInvokingLoadBalancerCreator,\n            StatusCodes.Status500InternalServerError)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Errors/ServicesAreEmptyError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.LoadBalancer.Errors;\n\npublic class ServicesAreEmptyError : Error\n{\n    public ServicesAreEmptyError(string message)\n        : base(message, OcelotErrorCode.ServicesAreEmptyError, StatusCodes.Status404NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Errors/ServicesAreNullError.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.LoadBalancer.Errors;\n\npublic class ServicesAreNullError : Error\n{\n    public ServicesAreNullError(string message)\n        : base(message, OcelotErrorCode.ServicesAreNullError, StatusCodes.Status404NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Errors/UnableToFindLoadBalancerError.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.LoadBalancer.Errors;\n\npublic class UnableToFindLoadBalancerError : Error\n{\n    public UnableToFindLoadBalancerError(string message)\n        : base(message, OcelotErrorCode.UnableToFindLoadBalancerError, StatusCodes.Status404NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Interfaces/ILoadBalancer.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Reflection;\n\nnamespace Ocelot.LoadBalancer.Interfaces;\n\n// TODO Add sync & async pairs\npublic interface ILoadBalancer\n{\n    Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext);\n\n    void Release(ServiceHostAndPort hostAndPort);\n\n    /// <summary>Static name of the load balancer instance.</summary>\n    /// <remarks>To avoid reflection calls of the <see cref=\"MemberInfo.Name\"/> property of the <see cref=\"System.Type\"/> objects.</remarks>\n    /// <value>A <see cref=\"string\"/> object with type name value.</value>\n    string Type { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.LoadBalancer.Interfaces;\n\npublic interface ILoadBalancerCreator\n{\n    Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider);\n    string Type { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerFactory.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.LoadBalancer.Interfaces;\n\npublic interface ILoadBalancerFactory\n{\n    Response<ILoadBalancer> Get(DownstreamRoute route, ServiceProviderConfiguration config);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerHouse.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.LoadBalancer.Interfaces;\n\npublic interface ILoadBalancerHouse\n{\n    Response<ILoadBalancer> Get(DownstreamRoute route, ServiceProviderConfiguration config);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/Lease.cs",
    "content": "using Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer;\n\npublic struct Lease : IEquatable<Lease>\n{\n    public Lease()\n    {\n        HostAndPort = null;\n        Connections = 0;\n    }\n\n    public Lease(Lease from)\n    {\n        HostAndPort = from.HostAndPort;\n        Connections = from.Connections;\n    }\n\n    public Lease(ServiceHostAndPort hostAndPort)\n    {\n        HostAndPort = hostAndPort;\n        Connections = 0;\n    }\n\n    public Lease(ServiceHostAndPort hostAndPort, int connections)\n    {\n        HostAndPort = hostAndPort;\n        Connections = connections;\n    }\n\n    public ServiceHostAndPort HostAndPort { get; }\n    public int Connections { get; set; }\n\n    public static Lease Null => new();\n\n    public override readonly string ToString() => $\"({HostAndPort}+{Connections})\";\n    public override readonly int GetHashCode() => HostAndPort.GetHashCode();\n    public override readonly bool Equals(object obj) => obj is Lease l && this == l;\n    public readonly bool Equals(Lease other) => this == other;\n\n    /// <summary>Checks equality of two leases.</summary>\n    /// <remarks>\n    /// <para>Override default implementation of <see cref=\"ValueType.Equals(object)\"/> because we want to ignore the <see cref=\"Connections\"/> property.</para>\n    /// Microsoft Learn | .NET | C# Docs:\n    ///   <list type=\"bullet\">\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/equality-operators\">Equality operators</seealso></item>\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-equals\">System.Object.Equals method</seealso></item>\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1.equals?view=net-8.0\">IEquatable&lt;T&gt;.Equals(T) Method</seealso></item>\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/api/system.valuetype.equals?view=net-8.0\">ValueType.Equals(Object) Method</seealso></item>\n    ///   </list>\n    /// </remarks>\n    /// <param name=\"x\">First operand.</param>\n    /// <param name=\"y\">Second operand.</param>\n    /// <returns><see langword=\"true\"/> if both operands are equal; otherwise, <see langword=\"false\"/>.</returns>\n    public static bool operator ==(Lease x, Lease y) => x.HostAndPort == y.HostAndPort; // ignore -> x.Connections == y.Connections;\n    public static bool operator !=(Lease x, Lease y) => !(x == y);\n\n    public static bool operator ==(ServiceHostAndPort h, Lease l) => h == l.HostAndPort;\n    public static bool operator !=(ServiceHostAndPort h, Lease l) => !(h == l);\n\n    public static bool operator ==(Lease l, ServiceHostAndPort h) => l.HostAndPort == h;\n    public static bool operator !=(Lease l, ServiceHostAndPort h) => !(l == h);\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/LeaseEventArgs.cs",
    "content": "﻿using Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer;\n\npublic class LeaseEventArgs : EventArgs\n{\n    public LeaseEventArgs(Lease lease, Service service, int serviceIndex)\n    {\n        Lease = lease;\n        Service = service;\n        ServiceIndex = serviceIndex;\n    }\n\n    public Lease Lease { get; }\n    public Service Service { get; }\n    public int ServiceIndex { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/LoadBalancerFactory.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\n\nnamespace Ocelot.LoadBalancer;\n\npublic class LoadBalancerFactory : ILoadBalancerFactory\n{\n    private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory;\n    private readonly IEnumerable<ILoadBalancerCreator> _loadBalancerCreators;\n\n    public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory, IEnumerable<ILoadBalancerCreator> loadBalancerCreators)\n    {\n        _serviceProviderFactory = serviceProviderFactory;\n        _loadBalancerCreators = loadBalancerCreators;\n    }\n\n    public Response<ILoadBalancer> Get(DownstreamRoute route, ServiceProviderConfiguration config)\n    {\n        var serviceProviderFactoryResponse = _serviceProviderFactory.Get(config, route);\n\n        if (serviceProviderFactoryResponse.IsError)\n        {\n            return new ErrorResponse<ILoadBalancer>(serviceProviderFactoryResponse.Errors);\n        }\n\n        var serviceProvider = serviceProviderFactoryResponse.Data;\n        var requestedType = route.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer);\n        var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType);\n\n        if (applicableCreator == null)\n        {\n            return new ErrorResponse<ILoadBalancer>(new CouldNotFindLoadBalancerCreatorError($\"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name.\"));\n        }\n\n        var createdLoadBalancerResponse = applicableCreator.Create(route, serviceProvider);\n\n        if (createdLoadBalancerResponse.IsError)\n        {\n            return new ErrorResponse<ILoadBalancer>(createdLoadBalancerResponse.Errors);\n        }\n\n        return new OkResponse<ILoadBalancer>(createdLoadBalancerResponse.Data);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/LoadBalancerHouse.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\n\nnamespace Ocelot.LoadBalancer;\n\npublic class LoadBalancerHouse : ILoadBalancerHouse\n{\n    private readonly ILoadBalancerFactory _factory;\n    private readonly Dictionary<string, ILoadBalancer> _loadBalancers;\n#if NET9_0_OR_GREATER\n    private static readonly Lock SyncRoot = new();\n#else\n    private static readonly object SyncRoot = new();\n#endif\n\n    public LoadBalancerHouse(ILoadBalancerFactory factory)\n    {\n        _factory = factory;\n        _loadBalancers = new();\n    }\n\n    public Response<ILoadBalancer> Get(DownstreamRoute route, ServiceProviderConfiguration config)\n    {\n        try\n        {\n            lock (SyncRoot)\n            {\n                return _loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer) &&\n                        loadBalancer.Type.Equals(route.LoadBalancerOptions.Type, StringComparison.OrdinalIgnoreCase)\n                    ? new OkResponse<ILoadBalancer>(loadBalancer)\n                    : GetResponse(route, config);\n            }\n        }\n        catch (Exception ex)\n        {\n            return new ErrorResponse<ILoadBalancer>(\n                new UnableToFindLoadBalancerError($\"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};\"));\n        }\n    }\n\n    private Response<ILoadBalancer> GetResponse(DownstreamRoute route, ServiceProviderConfiguration config)\n    {\n        var result = _factory.Get(route, config);\n        if (result.IsError)\n        {\n            return new ErrorResponse<ILoadBalancer>(result.Errors);\n        }\n\n        var balancer = result.Data;\n        _loadBalancers[route.LoadBalancerKey] = balancer; // TODO TryAdd ?\n        return new OkResponse<ILoadBalancer>(balancer);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/LoadBalancingMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.LoadBalancer;\n\npublic class LoadBalancingMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly ILoadBalancerHouse _loadBalancerHouse;\n\n    public LoadBalancingMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        ILoadBalancerHouse loadBalancerHouse)\n        : base(loggerFactory.CreateLogger<LoadBalancingMiddleware>())\n    {\n        _next = next;\n        _loadBalancerHouse = loadBalancerHouse;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n\n        var internalConfiguration = httpContext.Items.IInternalConfiguration();\n\n        var loadBalancer = _loadBalancerHouse.Get(downstreamRoute, internalConfiguration.ServiceProviderConfiguration);\n\n        if (loadBalancer.IsError)\n        {\n            httpContext.Items.UpsertErrors(loadBalancer.Errors);\n            return;\n        }\n\n        var hostAndPort = await loadBalancer.Data.LeaseAsync(httpContext);\n        if (hostAndPort.IsError)\n        {\n            httpContext.Items.UpsertErrors(hostAndPort.Errors);\n            return;\n        }\n\n        var downstreamRequest = httpContext.Items.DownstreamRequest();\n\n        //todo check downstreamRequest is ok\n        downstreamRequest.Host = hostAndPort.Data.DownstreamHost;\n\n        if (hostAndPort.Data.DownstreamPort > 0)\n        {\n            downstreamRequest.Port = hostAndPort.Data.DownstreamPort;\n        }\n\n        if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme))\n        {\n            downstreamRequest.Scheme = hostAndPort.Data.Scheme;\n        }\n\n        try\n        {\n            // If an exception occurs, the object will be handled by the global exception handler\n            await _next.Invoke(httpContext);\n        }\n        finally\n        {\n            loadBalancer.Data.Release(hostAndPort.Data);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/LoadBalancer/StickySession.cs",
    "content": "using Ocelot.Values;\n\nnamespace Ocelot.LoadBalancer;\n\npublic class StickySession\n{\n    public StickySession(ServiceHostAndPort hostAndPort, DateTime expiry, string key)\n    {\n        HostAndPort = hostAndPort;\n        Expiry = expiry;\n        Key = key;\n    }\n\n    public ServiceHostAndPort HostAndPort { get; }\n\n    public DateTime Expiry { get; }\n\n    public string Key { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/IOcelotLogger.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Infrastructure.RequestData;\n\nnamespace Ocelot.Logging;\n\n/// <summary>\n/// Thin wrapper around the .NET Core logging framework, used to allow the <see cref=\"IRequestScopedDataRepository\"/> object to be injected giving access to the Ocelot <see cref=\"IInternalConfiguration.RequestId\"/>.\n/// </summary>\npublic interface IOcelotLogger : IDisposable\n{\n    void LogTrace(string message);\n    void LogTrace(Func<string> messageFactory);\n\n    void LogDebug(string message);\n    void LogDebug(Func<string> messageFactory);\n\n    void LogInformation(string message);\n    void LogInformation(Func<string> messageFactory);\n\n    void LogWarning(string message);\n    void LogWarning(Func<string> messageFactory);\n\n    void LogError(string message, Exception exception);\n    void LogError(Func<string> messageFactory, Exception exception);\n\n    void LogCritical(string message, Exception exception);\n    void LogCritical(Func<string> messageFactory, Exception exception);\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/IOcelotLoggerFactory.cs",
    "content": "﻿namespace Ocelot.Logging;\r\n\r\npublic interface IOcelotLoggerFactory : IDisposable\r\n{\r\n    IOcelotLogger CreateLogger<T>();\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Logging/IOcelotTracer.cs",
    "content": "using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.Logging;\n\npublic interface IOcelotTracer\n{\n    void Event(HttpContext httpContext, string @event);\n\n    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,\n        Action<string> addTraceIdToRepo,\n        Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> baseSendAsync,\n        CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/ITracingHandler.cs",
    "content": "﻿namespace Ocelot.Logging;\n\npublic interface ITracingHandler\n{\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/ITracingHandlerFactory.cs",
    "content": "﻿namespace Ocelot.Logging;\n\npublic interface ITracingHandlerFactory\n{\n    ITracingHandler Get();\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/OcelotDiagnosticListener.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DiagnosticAdapter;\n\nnamespace Ocelot.Logging;\n\npublic class OcelotDiagnosticListener\n{\n    private readonly IOcelotLogger _logger;\n    private readonly IOcelotTracer _tracer;\n\n    public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider serviceProvider)\n    {\n        _logger = factory.CreateLogger<OcelotDiagnosticListener>();\n        _tracer = serviceProvider.GetService<IOcelotTracer>();\n    }\n\n    [DiagnosticName(\"Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting\")]\n    public virtual void OnMiddlewareStarting(HttpContext httpContext, string name)\n    {\n        _logger.LogTrace(() => $\"MiddlewareStarting: {name}; {httpContext.Request.Path}\");\n        Event(httpContext, $\"MiddlewareStarting: {name}; {httpContext.Request.Path}\");\n    }\n\n    [DiagnosticName(\"Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException\")]\n    public virtual void OnMiddlewareException(Exception exception, string name)\n    {\n        _logger.LogTrace(() => $\"MiddlewareException: {name}; {exception.Message};\");\n    }\n\n    [DiagnosticName(\"Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished\")]\n    public virtual void OnMiddlewareFinished(HttpContext httpContext, string name)\n    {\n        _logger.LogTrace(() => $\"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}\");\n        Event(httpContext, $\"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}\");\n    }\n\n    protected virtual void Event(HttpContext httpContext, string @event)\n    {\n        _tracer?.Event(httpContext, @event);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/OcelotHttpTracingHandler.cs",
    "content": "﻿using Ocelot.Infrastructure.RequestData;\n\nnamespace Ocelot.Logging;\n\npublic class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler\n{\n    public const string TraceId = nameof(TraceId);\n\n    private readonly IOcelotTracer _tracer;\n    private readonly IRequestScopedDataRepository _repo;\n\n    public OcelotHttpTracingHandler(\n        IOcelotTracer tracer,\n        IRequestScopedDataRepository repo,\n        HttpMessageHandler httpMessageHandler = null)\n    {\n        _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer));\n        _repo = repo ?? throw new ArgumentNullException(nameof(repo));\n        InnerHandler = httpMessageHandler ?? new HttpClientHandler();\n    }\n\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        => _tracer.SendAsync(request, AddTraceId, base.SendAsync, cancellationToken); // TODO This is absolutely wrong\n\n    protected virtual void AddTraceId(string id)\n        => _repo.Add(TraceId, id);\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/OcelotLogger.cs",
    "content": "using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.RequestId.Middleware;\n\nnamespace Ocelot.Logging;\n\n/// <summary>\n/// Default implementation of the <see cref=\"IOcelotLogger\"/> interface.\n/// </summary>\npublic class OcelotLogger : IOcelotLogger, IDisposable\n{\n    private readonly ILogger _logger;\n    private readonly IRequestScopedDataRepository _scopedDataRepository;\n    private bool _disposed;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"OcelotLogger\"/> class.\n    /// <para>\n    /// Please note:\n    /// the log event message is designed to use placeholders ({RequestId}, {PreviousRequestId}, and {Message}).\n    /// If you're using a logger like Serilog, it will automatically capture these as structured data properties, making it easier to query and analyze the logs later.\n    /// </para>\n    /// </summary>\n    /// <param name=\"logger\">The main logger type, per default the Microsoft implementation.</param>\n    /// <param name=\"scopedDataRepository\">Repository, saving and getting data to/from HttpContext.Items.</param>\n    /// <exception cref=\"ArgumentNullException\">The ILogger object is injected in OcelotLoggerFactory, it can't be verified before.</exception>\n    public OcelotLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository)\n    {\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _scopedDataRepository = scopedDataRepository ?? throw new ArgumentNullException(nameof(scopedDataRepository));\n    }\n\n    public void LogTrace(string message) => WriteLog(LogLevel.Trace, message);\n    public void LogTrace(Func<string> messageFactory) => WriteLog(LogLevel.Trace, messageFactory);\n\n    public void LogDebug(string message) => WriteLog(LogLevel.Debug, message);\n    public void LogDebug(Func<string> messageFactory) => WriteLog(LogLevel.Debug, messageFactory);\n\n    public void LogInformation(string message) => WriteLog(LogLevel.Information, message);\n    public void LogInformation(Func<string> messageFactory) => WriteLog(LogLevel.Information, messageFactory);\n\n    public void LogWarning(string message) => WriteLog(LogLevel.Warning, message);\n    public void LogWarning(Func<string> messageFactory) => WriteLog(LogLevel.Warning, messageFactory);\n\n    public void LogError(string message, Exception exception) => WriteLog(LogLevel.Error, message, exception);\n    public void LogError(Func<string> messageFactory, Exception exception) => WriteLog(LogLevel.Error, messageFactory, exception);\n\n    public void LogCritical(string message, Exception exception) => WriteLog(LogLevel.Critical, message, exception);\n    public void LogCritical(Func<string> messageFactory, Exception exception) => WriteLog(LogLevel.Critical, messageFactory, exception);\n\n    private string GetOcelotRequestId()\n    {\n        var requestId = _scopedDataRepository.Get<string>(RequestIdMiddleware.RequestIdName);\n        return requestId.IsError ? \"-\" : requestId.Data;\n    }\n\n    private string GetOcelotPreviousRequestId()\n    {\n        var requestId = _scopedDataRepository.Get<string>(RequestIdMiddleware.PreviousRequestIdName);\n        return requestId.IsError ? \"-\" : requestId.Data;\n    }\n\n    private void WriteLog(LogLevel logLevel, string message, Exception exception = null)\n    {\n        WriteLog(logLevel, null, message, exception);\n    }\n\n    private void WriteLog(LogLevel logLevel, Func<string> messageFactory, Exception exception = null)\n    {\n        WriteLog(logLevel, messageFactory, null, exception);\n    }\n\n    private void WriteLog(LogLevel logLevel, Func<string> messageFactory, string message, Exception exception = null)\n    {\n        if (_disposed || !_logger.IsEnabled(logLevel))\n            return;\n\n        var requestId = GetOcelotRequestId();\n        var previousRequestId = GetOcelotPreviousRequestId();\n        if (messageFactory != null)\n        {\n            message = messageFactory.Invoke();\n        }\n\n        try\n        {\n            _logger.Log(logLevel, default,\n                $\"{RequestIdMiddleware.RequestIdName}: {requestId}, {RequestIdMiddleware.PreviousRequestIdName}: {previousRequestId}{Environment.NewLine + message}\",\n                exception, NoFormatter);\n        }\n        catch (ObjectDisposedException)\n        {\n            // Logger factory or its providers have been disposed.\n            // This can happen when errors occur in background operations.\n            // Silently ignore to prevent cascading failures during shutdown.\n        }\n    }\n\n    public static string NoFormatter(string state, Exception e) => state;\n    public static string ExceptionFormatter(string state, Exception e)\n        => e == null ? state : $\"{state}, {Environment.NewLine + nameof(Exception)}: {e}\";\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n            return;\n\n        if (disposing)\n        {\n        }\n\n        _disposed = true;\n    }\n\n    public void Dispose()\n    {\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/OcelotLoggerFactory.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\n\nnamespace Ocelot.Logging;\n\npublic class OcelotLoggerFactory : IOcelotLoggerFactory, IDisposable\n{\n    private readonly ILoggerFactory _loggerFactory;\n    private readonly IRequestScopedDataRepository _scopedDataRepository;\n    private bool _disposed;\n\n    public OcelotLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository)\n    {\n        _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));\n        _scopedDataRepository = scopedDataRepository ?? throw new ArgumentNullException(nameof(scopedDataRepository));\n    }\n\n    public IOcelotLogger CreateLogger<T>()\n    {\n        ObjectDisposedException.ThrowIf(_disposed, this);\n        var logger = _loggerFactory.CreateLogger<T>();\n        return new OcelotLogger(logger, _scopedDataRepository);\n    }\n\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n            return;\n\n        if (disposing)\n        {\n            _loggerFactory.Dispose();\n        }\n\n        _disposed = true;\n    }\n\n    ~OcelotLoggerFactory() => Dispose(false);\n}\n"
  },
  {
    "path": "src/Ocelot/Logging/TracingHandlerFactory.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Infrastructure.RequestData;\n\nnamespace Ocelot.Logging;\n\npublic class TracingHandlerFactory : ITracingHandlerFactory\n{\n    private readonly IOcelotTracer _tracer;\n    private readonly IRequestScopedDataRepository _repo;\n\n    public TracingHandlerFactory(\n        IServiceProvider services,\n        IRequestScopedDataRepository repo)\n    {\n        _repo = repo;\n        _tracer = services.GetService<IOcelotTracer>();\n    }\n\n    public ITracingHandler Get()\n    {\n        return new OcelotHttpTracingHandler(_tracer, _repo);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Metadata/DownstreamRouteExtensions.cs",
    "content": "﻿using Ocelot.Configuration;\nusing System.Globalization;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\n\nnamespace Ocelot.Metadata;\n\npublic static class DownstreamRouteExtensions\n{\n    /// <summary>\n    /// The known truthy values.\n    /// </summary>\n    private static readonly HashSet<string> TruthyValues =\n        new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"true\",\n            \"yes\",\n            \"on\",\n            \"ok\",\n            \"enable\",\n            \"enabled\",\n            \"1\",\n        };\n\n    /// <summary>\n    /// The known falsy values.\n    /// </summary>\n    private static readonly HashSet<string> FalsyValues =\n        new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"false\",\n            \"no\",\n            \"off\",\n            \"disable\",\n            \"disabled\",\n            \"0\",\n        };\n\n    /// <summary>\n    /// The known numeric types.\n    /// </summary>\n    private static readonly HashSet<Type> NumericTypes = new()\n    {\n        typeof(byte),\n        typeof(sbyte),\n        typeof(short),\n        typeof(ushort),\n        typeof(int),\n        typeof(uint),\n        typeof(long),\n        typeof(ulong),\n        typeof(float),\n        typeof(double),\n        typeof(decimal),\n    };\n\n    /// <summary>\n    /// Gets metadata from a downstream route.\n    /// </summary>\n    /// <typeparam name=\"T\">The metadata target type.</typeparam>\n    /// <param name=\"route\">The current downstream route.</param>\n    /// <param name=\"key\">The metadata key in downstream route Metadata dictionary.</param>\n    /// <param name=\"defaultValue\">The fallback value if no value found.</param>\n    /// <param name=\"options\">Custom json serializer options if needed.</param>\n    /// <returns>A parsed metadata value of the <typeparamref name=\"T\"/> type.</returns>\n    public static T GetMetadata<T>(this DownstreamRoute route, string key, T defaultValue = default, JsonSerializerOptions options = null)\n    {\n        var metadata = route?.MetadataOptions.Metadata;\n        return (metadata == null || !metadata.TryGetValue(key, out var metadataValue) || metadataValue == null)\n            ? defaultValue\n            : (T)ConvertTo(typeof(T), metadataValue, route.MetadataOptions, options ?? new(JsonSerializerDefaults.Web));\n    }\n\n    // TODO See Metadata sample\n    private static JsonNode GetMetadataNode(this DownstreamRoute route, string key, JsonNode defaultValue = default, JsonSerializerOptions options = null)\n    {\n        var metadata = route?.MetadataOptions.Metadata;\n        return null;\n    }\n\n    /// <summary>\n    /// Converting a string value to the target type.\n    /// Some custom conversion has been for the following types:\n    /// <see cref=\"bool\"/>, <see langword=\"bool?\"/>, <see cref=\"Array\"/>, numeric types;\n    /// otherwise trying to deserialize the value using the JsonSerializer.\n    /// </summary>\n    /// <param name=\"targetType\">The target type.</param>\n    /// <param name=\"value\">The string value.</param>\n    /// <param name=\"options\">The metadata options, it includes the global configuration.</param>\n    /// <param name=\"jsonOptions\">If needed, some custom json serializer options.</param>\n    /// <returns>The converted string.</returns>\n    private static object ConvertTo(Type targetType, string value, MetadataOptions options, JsonSerializerOptions jsonOptions)\n    {\n        if (targetType == typeof(string))\n        {\n            return value;\n        }\n\n        if (targetType == typeof(bool))\n        {\n            return TruthyValues.Contains(value.Trim());\n        }\n\n        if (targetType == typeof(bool?))\n        {\n            return TruthyValues.Contains(value.Trim())\n                ? true\n                : FalsyValues.Contains(value.Trim()) ? false : null;\n        }\n\n        if (targetType == typeof(string[]))\n        {\n            return (value == null)\n                ? Array.Empty<string>()\n                : value.Split(options.Separators, options.StringSplitOption)\n                    .Select(s => s.Trim(options.TrimChars))\n                    .Where(s => !string.IsNullOrEmpty(s))\n                    .ToArray();\n        }\n\n        return NumericTypes.Contains(targetType)\n            ? ConvertToNumericType(value, targetType, options.CurrentCulture, options.NumberStyle)\n            : JsonSerializer.Deserialize(value, targetType, jsonOptions);\n    }\n\n    /// <summary>\n    /// Converting string to the known numeric types.\n    /// </summary>\n    /// <param name=\"value\">The number as string.</param>\n    /// <param name=\"targetType\">The target numeric type.</param>\n    /// <param name=\"provider\">The current format provider.</param>\n    /// <param name=\"numberStyle\">The current number style configuration.</param>\n    /// <returns>The parsed string as object of type targetType.</returns>\n    /// <exception cref=\"NotImplementedException\">Exception thrown if the conversion for the type target type can't be found.</exception>\n    private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, NumberStyles numberStyle)\n    {\n        return targetType switch\n        {\n            { } t when t == typeof(byte) => byte.Parse(value, numberStyle, provider),\n            { } t when t == typeof(sbyte) => sbyte.Parse(value, numberStyle, provider),\n            { } t when t == typeof(short) => short.Parse(value, numberStyle, provider),\n            { } t when t == typeof(ushort) => ushort.Parse(value, numberStyle, provider),\n            { } t when t == typeof(int) => int.Parse(value, numberStyle, provider),\n            { } t when t == typeof(uint) => uint.Parse(value, numberStyle, provider),\n            { } t when t == typeof(long) => long.Parse(value, numberStyle, provider),\n            { } t when t == typeof(ulong) => ulong.Parse(value, numberStyle, provider),\n            { } t when t == typeof(float) => float.Parse(value, numberStyle, provider),\n            { } t when t == typeof(double) => double.Parse(value, numberStyle, provider),\n            { } t when t == typeof(decimal) => decimal.Parse(value, numberStyle, provider),\n            _ => throw new NotImplementedException($\"No conversion available for the type: {targetType.Name}\"),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/BaseUrlFinder.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.Middleware;\n\npublic class BaseUrlFinder : IBaseUrlFinder\n{\n    private readonly IConfiguration _config;\n\n    public BaseUrlFinder(IConfiguration config)\n    {\n        _config = config;\n    }\n\n    public string Find()\n    {\n        // Tries to get base url out of file...\n        var key = $\"{nameof(FileConfiguration.GlobalConfiguration)}:{nameof(FileGlobalConfiguration.BaseUrl)}\";\n        var baseUrl = _config.GetValue(key, string.Empty);\n\n        // Falls back to memory config then finally default..\n        return string.IsNullOrEmpty(baseUrl)\n            ? _config.GetValue(nameof(FileGlobalConfiguration.BaseUrl), \"http://localhost:5000\")\n            : baseUrl;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/ConfigurationMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Errors.Middleware;\nusing Ocelot.Logging;\n\nnamespace Ocelot.Middleware;\n\npublic class ConfigurationMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IInternalConfigurationRepository _configRepo;\n\n    public ConfigurationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IInternalConfigurationRepository configRepo)\n        : base(loggerFactory.CreateLogger<ConfigurationMiddleware>())\n    {\n        _next = next;\n        _configRepo = configRepo;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        //todo check the config is actually ok?\n        var config = _configRepo.Get();\n\n        if (config.IsError)\n        {\n            throw new System.Exception(\"OOOOPS this should not happen raise an issue in GitHub\");\n        }\n\n        httpContext.Items.SetIInternalConfiguration(config.Data);\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/DownstreamResponse.cs",
    "content": "namespace Ocelot.Middleware;\n\npublic class DownstreamResponse : IDisposable\n{\n    // To detect redundant calls\n    private bool _disposedValue;\n    private readonly HttpResponseMessage _responseMessage;\n\n    public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, List<Header> headers,\n        string reasonPhrase)\n    {\n        Content = content;\n        StatusCode = statusCode;\n        Headers = headers ?? new();\n        ReasonPhrase = reasonPhrase;\n    }\n\n    public DownstreamResponse(HttpResponseMessage response)\n        : this(response.Content, response.StatusCode,\n            response.Headers.Select(x => new Header(x.Key, x.Value)).ToList(), response.ReasonPhrase)\n    {\n        _responseMessage = response;\n    }\n\n    public DownstreamResponse(HttpContent content, HttpStatusCode statusCode,\n        IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers, string reasonPhrase)\n        : this(content, statusCode, headers.Select(x => new Header(x.Key, x.Value)).ToList(), reasonPhrase)\n    {\n    }\n\n    public HttpContent Content { get; }\n    public HttpStatusCode StatusCode { get; }\n    public List<Header> Headers { get; }\n    public string ReasonPhrase { get; }\n\n    // Public implementation of Dispose pattern callable by consumers.\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    /// <summary>\n    /// We should make sure we dispose the content and response message to close the connection to the downstream service.\n    /// </summary>\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposedValue)\n        {\n            return;\n        }\n\n        if (disposing)\n        {\n            Content?.Dispose();\n            _responseMessage?.Dispose();\n        }\n\n        _disposedValue = true;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/Header.cs",
    "content": "namespace Ocelot.Middleware;\n\npublic class Header\n{\n    public Header(string key, IEnumerable<string> values)\n    {\n        Key = key;\n        Values = values ?? new List<string>();\n    }\n\n    public string Key { get; }\n    public IEnumerable<string> Values { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/HttpItemsExtensions.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Errors;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.Middleware;\n\npublic static class HttpItemsExtensions\n{\n    public static void UpsertDownstreamRequest(this IDictionary<object, object> input, DownstreamRequest downstreamRequest)\n    {\n        input.Upsert(\"DownstreamRequest\", downstreamRequest);\n    }\n\n    public static void UpsertDownstreamResponse(this IDictionary<object, object> input, DownstreamResponse downstreamResponse)\n    {\n        input.Upsert(\"DownstreamResponse\", downstreamResponse);\n    }\n\n    public static void UpsertDownstreamRoute(this IDictionary<object, object> input, DownstreamRoute downstreamRoute)\n    {\n        input.Upsert(\"DownstreamRoute\", downstreamRoute);\n    }\n\n    public static void UpsertTemplatePlaceholderNameAndValues(this IDictionary<object, object> input, List<PlaceholderNameAndValue> tPNV)\n    {\n        input.Upsert(\"TemplatePlaceholderNameAndValues\", tPNV);\n    }\n\n    public static void UpsertDownstreamRoute(this IDictionary<object, object> input, DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute)\n    {\n        input.Upsert(\"DownstreamRouteHolder\", downstreamRoute);\n    }\n\n    public static void UpsertErrors(this IDictionary<object, object> input, List<Error> errors)\n    {\n        input.Upsert(\"Errors\", errors);\n    }\n\n    public static void SetError(this IDictionary<object, object> input, Error error)\n    {\n        var errors = new List<Error> { error };\n        input.Upsert(\"Errors\", errors);\n    }\n\n    public static void SetIInternalConfiguration(this IDictionary<object, object> input, IInternalConfiguration config)\n    {\n        input.Upsert(\"IInternalConfiguration\", config);\n    }\n\n    public static IInternalConfiguration IInternalConfiguration(this IDictionary<object, object> input)\n    {\n        return input.Get<IInternalConfiguration>(\"IInternalConfiguration\");\n    }\n\n    public static List<Error> Errors(this IDictionary<object, object> input)\n    {\n        var errors = input.Get<List<Error>>(\"Errors\");\n        return errors ?? new();\n    }\n\n    public static DownstreamRouteFinder.DownstreamRouteHolder\n        DownstreamRouteHolder(this IDictionary<object, object> input) =>\n        input.Get<DownstreamRouteFinder.DownstreamRouteHolder>(\"DownstreamRouteHolder\");\n\n    public static List<PlaceholderNameAndValue>\n        TemplatePlaceholderNameAndValues(this IDictionary<object, object> input) =>\n        input.Get<List<PlaceholderNameAndValue>>(\"TemplatePlaceholderNameAndValues\");\n\n    public static DownstreamRequest DownstreamRequest(this IDictionary<object, object> input) =>\n        input.Get<DownstreamRequest>(\"DownstreamRequest\");\n\n    public static DownstreamResponse DownstreamResponse(this IDictionary<object, object> input) =>\n        input.Get<DownstreamResponse>(\"DownstreamResponse\");\n\n    public static DownstreamRoute DownstreamRoute(this IDictionary<object, object> input) =>\n        input.Get<DownstreamRoute>(\"DownstreamRoute\");\n\n    private static T Get<T>(this IDictionary<object, object> input, string key) =>\n    input.TryGetValue(key, out var value) ? (T)value : default;\n\n    private static void Upsert<T>(this IDictionary<object, object> input, string key, T value)\n    {\n        if (input.DoesntExist(key))\n        {\n            input.Add(key, value);\n        }\n        else\n        {\n            input.Remove(key);\n            input.Add(key, value);\n        }\n    }\n\n    private static bool DoesntExist(this IDictionary<object, object> input, string key) => !input.ContainsKey(key);\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/IBaseUrlFinder.cs",
    "content": "﻿namespace Ocelot.Middleware;\r\n\r\npublic interface IBaseUrlFinder\r\n{\r\n    string Find();\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Middleware/OcelotMiddleware.cs",
    "content": "﻿using Ocelot.Logging;\n\nnamespace Ocelot.Middleware;\n\npublic abstract class OcelotMiddleware\n{\n    protected OcelotMiddleware(IOcelotLogger logger)\n    {\n        Logger = logger;\n        MiddlewareName = GetType().Name;\n    }\n\n    public IOcelotLogger Logger { get; }\n    public string MiddlewareName { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\n\nnamespace Ocelot.Middleware;\n\npublic delegate Task OcelotMiddlewareConfigurationDelegate(IApplicationBuilder builder);\n"
  },
  {
    "path": "src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Administration;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Configuration.Setter;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\nusing System.Diagnostics;\n\nnamespace Ocelot.Middleware;\n\npublic static class OcelotMiddlewareExtensions\n{\n    public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder)\n    {\n        await builder.UseOcelot(new OcelotPipelineConfiguration());\n        return builder;\n    }\n\n    public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, Action<OcelotPipelineConfiguration> pipelineConfiguration)\n    {\n        var config = new OcelotPipelineConfiguration();\n        pipelineConfiguration?.Invoke(config);\n        return await builder.UseOcelot(config);\n    }\n\n    public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)\n    {\n        _ = await CreateConfiguration(builder);\n\n        ConfigureDiagnosticListener(builder);\n\n        return CreateOcelotPipeline(builder, pipelineConfiguration);\n    }\n\n    public static Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder app, Action<IApplicationBuilder, OcelotPipelineConfiguration> builderAction)\n        => UseOcelot(app, builderAction, new OcelotPipelineConfiguration());\n\n    public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder app, Action<IApplicationBuilder, OcelotPipelineConfiguration> builderAction, OcelotPipelineConfiguration configuration)\n    {\n        await CreateConfiguration(app);\n\n        ConfigureDiagnosticListener(app);\n\n        builderAction?.Invoke(app, configuration ?? new OcelotPipelineConfiguration());\n\n        app.Properties[\"analysis.NextMiddlewareName\"] = \"TransitionToOcelotMiddleware\";\n\n        return app;\n    }\n\n    private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)\n    {\n        builder.BuildOcelotPipeline(pipelineConfiguration);\n\n        /*\n        inject first delegate into first piece of asp.net middleware..maybe not like this\n        then because we are updating the http context in ocelot it comes out correct for\n        rest of asp.net..\n        */\n\n        builder.Properties[\"analysis.NextMiddlewareName\"] = \"TransitionToOcelotMiddleware\";\n\n        return builder;\n    }\n\n    private static async Task<IInternalConfiguration> CreateConfiguration(IApplicationBuilder builder)\n    {\n        // make configuration from file system?\n        // earlier user needed to add ocelot files in startup configuration stuff, asp.net will map it to this\n        var fileConfig = builder.ApplicationServices.GetService<IOptionsMonitor<FileConfiguration>>();\n\n        // now create the config\n        var internalConfigCreator = builder.ApplicationServices.GetService<IInternalConfigurationCreator>();\n        var internalConfig = await internalConfigCreator.Create(fileConfig.CurrentValue);\n\n        //Configuration error, throw error message\n        if (internalConfig.IsError)\n        {\n            ThrowToStopOcelotStarting(internalConfig);\n        }\n\n        // now save it in memory\n        var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();\n        internalConfigRepo.AddOrReplace(internalConfig.Data);\n\n        fileConfig.OnChange(async (config) =>\n        {\n            var newInternalConfig = await internalConfigCreator.Create(config);\n            internalConfigRepo.AddOrReplace(newInternalConfig.Data);\n        });\n\n        var adminPath = builder.ApplicationServices.GetService<IAdministrationPath>();\n\n        var configurations = builder.ApplicationServices.GetServices<OcelotMiddlewareConfigurationDelegate>();\n\n        // Todo - this has just been added for consul so far...will there be an ordering problem in the future? Should refactor all config into this pattern?\n        foreach (var configuration in configurations)\n        {\n            await configuration(builder);\n        }\n\n        if (AdministrationApiInUse(adminPath))\n        {\n            //We have to make sure the file config is set for the ocelot.env.json and ocelot.json so that if we pull it from the\n            //admin api it works...boy this is getting a spit spags boll.\n            var fileConfigSetter = builder.ApplicationServices.GetService<IFileConfigurationSetter>();\n\n            await SetFileConfig(fileConfigSetter, fileConfig);\n        }\n\n        return GetOcelotConfigAndReturn(internalConfigRepo);\n    }\n\n    private static bool AdministrationApiInUse(IAdministrationPath adminPath)\n    {\n        return adminPath != null;\n    }\n\n    private static async Task SetFileConfig(IFileConfigurationSetter fileConfigSetter, IOptionsMonitor<FileConfiguration> fileConfig)\n    {\n        var response = await fileConfigSetter.Set(fileConfig.CurrentValue);\n\n        if (IsError(response))\n        {\n            ThrowToStopOcelotStarting(response);\n        }\n    }\n\n    private static bool IsError(Response response)\n    {\n        return response == null || response.IsError;\n    }\n\n    private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigurationRepository provider)\n    {\n        var ocelotConfiguration = provider.Get();\n\n        if (ocelotConfiguration?.Data == null || ocelotConfiguration.IsError)\n        {\n            ThrowToStopOcelotStarting(ocelotConfiguration);\n        }\n\n        return ocelotConfiguration.Data;\n    }\n\n    private static void ThrowToStopOcelotStarting(Response config)\n    {\n        throw new Exception($\"Unable to start Ocelot, errors are:{config.Errors.ToErrorString(true, true)}\");\n    }\n\n    private static void ConfigureDiagnosticListener(IApplicationBuilder builder)\n    {\n        _ = builder.ApplicationServices.GetService<IWebHostEnvironment>();\n        var listener = builder.ApplicationServices.GetService<OcelotDiagnosticListener>();\n        var diagnosticListener = builder.ApplicationServices.GetService<DiagnosticListener>();\n        diagnosticListener.SubscribeWithAdapter(listener);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/OcelotPipelineConfiguration.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.Middleware;\n\npublic class OcelotPipelineConfiguration\n{\n    /// <summary>\n    /// This is called after the global error handling middleware so any code before calling next.invoke\n    /// is the next thing called in the Ocelot pipeline. Anything after next.invoke is the last thing called\n    /// in the Ocelot pipeline before we go to the global error handler.\n    /// </summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> PreErrorResponderMiddleware { get; set; }\n\n    /// <summary>This allows the user to completely override Ocelot's <see cref=\"Responder.Middleware.ResponderMiddleware\"/>.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> ResponderMiddleware { get; set; }\n\n    /// <summary>This is to allow the user to run any extra authentication before the Ocelot authentication kicks in.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> PreAuthenticationMiddleware { get; set; }\n\n    /// <summary>This allows the user to completely override Ocelot's <see cref=\"Authentication.AuthenticationMiddleware\"/>.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> AuthenticationMiddleware { get; set; }\n\n    /// <summary>This is to allow the user to run any extra authorization before the Ocelot authorization kicks in.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> PreAuthorizationMiddleware { get; set; }\n\n    /// <summary>This allows the user to completely override Ocelot's <see cref=\"Authorization.AuthorizationMiddleware\"/>.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> AuthorizationMiddleware { get; set; }\n\n    /// <summary>This allows the user to completely override Ocelot's <see cref=\"Headers.Middleware.ClaimsToHeadersMiddleware\"/>.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> ClaimsToHeadersMiddleware { get; set; }\n\n    /// <summary>This allows the user to implement there own query string manipulation logic.</summary>\n    /// <value>A <see cref=\"Func{T1, T2, T3}\"/> delegate object.</value>\n    public Func<HttpContext, Func<Task>, Task> PreQueryStringBuilderMiddleware { get; set; }\n\n    /// <summary>This is an extension that will branch to different pipes.</summary>\n    /// <value>A <see cref=\"Dictionary{TFunc, TAction}\"/> collection.</value>\n    public Dictionary<Func<HttpContext, bool>, Action<IApplicationBuilder>> MapWhenOcelotPipeline { get; } = new(); // TODO fix this data structure\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/OcelotPipelineExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Authentication;\nusing Ocelot.Authorization;\nusing Ocelot.Cache;\nusing Ocelot.Claims.Middleware;\nusing Ocelot.DownstreamPathManipulation.Middleware;\nusing Ocelot.DownstreamRouteFinder.Middleware;\nusing Ocelot.DownstreamUrlCreator;\nusing Ocelot.Errors.Middleware;\nusing Ocelot.Headers.Middleware;\nusing Ocelot.LoadBalancer;\nusing Ocelot.Multiplexer;\nusing Ocelot.QueryStrings;\nusing Ocelot.RateLimiting;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Requester.Middleware;\nusing Ocelot.RequestId.Middleware;\nusing Ocelot.Responder.Middleware;\nusing Ocelot.Security.Middleware;\nusing Ocelot.WebSockets;\n\nnamespace Ocelot.Middleware;\n\npublic static class OcelotPipelineExtensions\n{\n    public static RequestDelegate BuildOcelotPipeline(this IApplicationBuilder app, OcelotPipelineConfiguration configuration)\n    {\n        // this sets up the downstream context and gets the config\n        app.UseMiddleware<ConfigurationMiddleware>();\n\n        // This is registered to catch any global exceptions that are not handled\n        // It also sets the Request Id if anything is set globally\n        app.UseMiddleware<ExceptionHandlerMiddleware>();\n\n        // If the request is for websockets upgrade we fork into a different pipeline\n        app.MapWhen(httpContext => httpContext.WebSockets.IsWebSocketRequest,\n            ws =>\n            {\n                ws.UseMiddleware<DownstreamRouteFinderMiddleware>();\n                ws.UseMiddleware<MultiplexingMiddleware>();\n                ws.UseMiddleware<DownstreamRequestInitialiserMiddleware>();\n                ws.UseMiddleware<LoadBalancingMiddleware>();\n                ws.UseMiddleware<DownstreamUrlCreatorMiddleware>();\n                ws.UseMiddleware<WebSocketsProxyMiddleware>();\n            });\n\n        // Allow the user to respond with absolutely anything they want.\n        app.UseIfNotNull(configuration.PreErrorResponderMiddleware);\n\n        // This is registered first so it can catch any errors and issue an appropriate response\n        app.UseIfNotNull<ResponderMiddleware>(configuration.ResponderMiddleware);\n\n        // Then we get the downstream route information\n        app.UseMiddleware<DownstreamRouteFinderMiddleware>();\n\n        // Multiplex the request if required\n        app.UseMiddleware<MultiplexingMiddleware>();\n\n        // This security module, IP whitelist blacklist, extended security mechanism\n        app.UseMiddleware<SecurityMiddleware>();\n\n        //Expand other branch pipes\n        if (configuration.MapWhenOcelotPipeline != null)\n        {\n            foreach (var pipeline in configuration.MapWhenOcelotPipeline)\n            {\n                // todo why is this asking for an app app?\n                app.MapWhen(pipeline.Key, pipeline.Value);\n            }\n        }\n\n        // Now we have the ds route we can transform headers and stuff?\n        app.UseMiddleware<HttpHeadersTransformationMiddleware>();\n\n        // Initialises downstream request\n        app.UseMiddleware<DownstreamRequestInitialiserMiddleware>();\n\n        // We check whether the request is ratelimit, and if there is no continue processing\n        app.UseMiddleware<RateLimitingMiddleware>();\n\n        // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware)\n        // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten\n        // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware.\n        app.UseMiddleware<RequestIdMiddleware>();\n\n        // Allow pre authentication logic. The idea being people might want to run something custom before what is built in.\n        app.UseIfNotNull(configuration.PreAuthenticationMiddleware);\n\n        // Now we know where the client is going to go we can authenticate them.\n        // We allow the Ocelot middleware to be overriden by whatever the user wants.\n        app.UseIfNotNull<AuthenticationMiddleware>(configuration.AuthenticationMiddleware);\n\n        // The next thing we do is look at any claims transforms in case this is important for authorization\n        app.UseMiddleware<ClaimsToClaimsMiddleware>();\n\n        // Allow pre authorization logic. The idea being people might want to run something custom before what is built in.\n        app.UseIfNotNull(configuration.PreAuthorizationMiddleware);\n\n        // Now we have authenticated and done any claims transformation, we can authorize the request by AuthorizationMiddleware.\n        // We allow the Ocelot middleware to be overriden by whatever the user wants.\n        app.UseIfNotNull<AuthorizationMiddleware>(configuration.AuthorizationMiddleware);\n\n        // Now we can run the ClaimsToHeadersMiddleware: we allow the Ocelot middleware to be overriden by whatever the user wants.\n        app.UseIfNotNull<ClaimsToHeadersMiddleware>(configuration.ClaimsToHeadersMiddleware);\n\n        // Allow the user to implement their own query string manipulation logic\n        app.UseIfNotNull(configuration.PreQueryStringBuilderMiddleware);\n\n        // Now we can run any claims to query string transformation middleware\n        app.UseMiddleware<ClaimsToQueryStringMiddleware>();\n\n        app.UseMiddleware<ClaimsToDownstreamPathMiddleware>();\n\n        // Get the load balancer for this request\n        app.UseMiddleware<LoadBalancingMiddleware>();\n\n        // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used\n        app.UseMiddleware<DownstreamUrlCreatorMiddleware>();\n\n        // Not sure if this is the best place for this but we use the downstream url\n        // as the basis for our cache key.\n        app.UseMiddleware<OutputCacheMiddleware>();\n\n        //We fire off the request and set the response on the scoped data repo\n        app.UseMiddleware<HttpRequesterMiddleware>();\n\n        return app.Build();\n    }\n\n    private static IApplicationBuilder UseIfNotNull(this IApplicationBuilder builder, Func<HttpContext, Func<Task>, Task> middleware)\n        => middleware != null ? builder.Use(middleware) : builder;\n\n    private static IApplicationBuilder UseIfNotNull<TMiddleware>(this IApplicationBuilder builder, Func<HttpContext, Func<Task>, Task> middleware)\n        where TMiddleware : OcelotMiddleware => middleware != null\n            ? builder.Use(middleware)\n            : builder.UseMiddleware<TMiddleware>();\n}\n"
  },
  {
    "path": "src/Ocelot/Middleware/UnauthenticatedError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Middleware;\n\npublic class UnauthenticatedError : Error\n{\n    public UnauthenticatedError(string message)\n        : base(message, OcelotErrorCode.UnauthenticatedError, 401)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/CouldNotFindAggregatorError.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.Multiplexer;\n\npublic class CouldNotFindAggregatorError : Error\n{\n    public CouldNotFindAggregatorError(string aggregator)\n        : base($\"Could not find Aggregator: {aggregator}\", OcelotErrorCode.CouldNotFindAggregatorError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/IDefinedAggregator.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Multiplexer;\n\npublic interface IDefinedAggregator\n{\n    Task<DownstreamResponse> Aggregate(List<HttpContext> responses);\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/IDefinedAggregatorProvider.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Multiplexer;\n\npublic interface IDefinedAggregatorProvider\n{\n    Response<IDefinedAggregator> Get(Route route);\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/IResponseAggregator.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\n\nnamespace Ocelot.Multiplexer;\n\npublic interface IResponseAggregator\n{\n    Task Aggregate(Route route, HttpContext originalContext, List<HttpContext> downstreamResponses);\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/IResponseAggregatorFactory.cs",
    "content": "using Ocelot.Configuration;\n\nnamespace Ocelot.Multiplexer;\n\npublic interface IResponseAggregatorFactory\n{\n    IResponseAggregator Get(Route route);\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs",
    "content": "using Ocelot.Configuration;\n\nnamespace Ocelot.Multiplexer;\n\npublic class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory\n{\n    private readonly UserDefinedResponseAggregator _userDefined;\n    private readonly IResponseAggregator _simple;\n\n    public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider, IResponseAggregator responseAggregator)\n    {\n        _userDefined = new UserDefinedResponseAggregator(provider);\n        _simple = responseAggregator;\n    }\n\n    public IResponseAggregator Get(Route route)\n        => !string.IsNullOrEmpty(route.Aggregator) ? _userDefined : _simple;\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/MultiplexingMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Newtonsoft.Json.Linq;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Collections;\nusing Route = Ocelot.Configuration.Route;\n\nnamespace Ocelot.Multiplexer;\n\npublic class MultiplexingMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IResponseAggregatorFactory _factory;\n    private const string RequestIdString = \"RequestId\";\n\n    public MultiplexingMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IResponseAggregatorFactory factory)\n        : base(loggerFactory.CreateLogger<MultiplexingMiddleware>())\n    {\n        _factory = factory;\n        _next = next;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder();\n        var route = downstreamRouteHolder.Route;\n        var downstreamRoutes = route.DownstreamRoute;\n\n        // Case 1: if websocket request or single downstream route\n        if (ShouldProcessSingleRoute(httpContext, downstreamRoutes))\n        {\n            await ProcessSingleRouteAsync(httpContext, downstreamRoutes[0]);\n            return;\n        }\n\n        // Case 2: if no downstream routes\n        if (downstreamRoutes.Count == 0)\n        {\n            return;\n        }\n\n        // Case 3: if multiple downstream routes\n        var routeKeysConfigs = route.DownstreamRouteConfig;\n        if (routeKeysConfigs == null || routeKeysConfigs.Count == 0)\n        {\n            await ProcessRoutesAsync(httpContext, route);\n            return;\n        }\n\n        // Case 4: if multiple downstream routes with route keys\n        var mainResponseContext = await ProcessMainRouteAsync(httpContext, downstreamRoutes[0]);\n        if (mainResponseContext == null)\n        {\n            return;\n        }\n\n        var responsesContexts = await ProcessRoutesWithRouteKeysAsync(httpContext, downstreamRoutes, routeKeysConfigs, mainResponseContext);\n        if (responsesContexts.Length == 0)\n        {\n            return;\n        }\n\n        await MapResponsesAsync(httpContext, route, mainResponseContext, responsesContexts);\n    }\n\n    /// <summary>\n    /// Helper method to determine if only the first downstream route should be processed.\n    /// It is the case if the request is a websocket request or if there is only one downstream route.\n    /// </summary>\n    /// <param name=\"context\">The http context.</param>\n    /// <param name=\"routes\">The downstream routes.</param>\n    /// <returns>True if only the first downstream route should be processed.</returns>\n    private static bool ShouldProcessSingleRoute(HttpContext context, ICollection routes)\n        => context.WebSockets.IsWebSocketRequest || routes.Count == 1;\n\n    /// <summary>\n    /// Processing a single downstream route (no route keys).\n    /// In that case, no need to make copies of the http context.\n    /// </summary>\n    /// <param name=\"context\">The http context.</param>\n    /// <param name=\"route\">The downstream route.</param>\n    /// <returns>A <see cref=\"Task\"/> representing the asynchronous operation.</returns>\n    protected virtual Task ProcessSingleRouteAsync(HttpContext context, DownstreamRoute route)\n    {\n        context.Items.UpsertDownstreamRoute(route);\n        return _next.Invoke(context);\n    }\n\n    /// <summary>\n    /// Processing the downstream routes (no route keys).\n    /// </summary>\n    /// <param name=\"context\">The main http context.</param>\n    /// <param name=\"route\">The route.</param>\n    private async Task ProcessRoutesAsync(HttpContext context, Route route)\n    {\n        var tasks = route.DownstreamRoute\n            .Select(downstreamRoute => ProcessRouteAsync(context, downstreamRoute))\n            .ToArray();\n        var contexts = await Task.WhenAll(tasks);\n        await MapAsync(context, route, new(contexts));\n    }\n\n    /// <summary>\n    /// When using route keys, the first route is the main route and the rest are additional routes.\n    /// Since we need to break if the main route response is null, we must process the main route first.\n    /// </summary>\n    /// <param name=\"context\">The http context.</param>\n    /// <param name=\"route\">The first route, the main route.</param>\n    /// <returns>The updated http context.</returns>\n    private async Task<HttpContext> ProcessMainRouteAsync(HttpContext context, DownstreamRoute route)\n    {\n        context.Items.UpsertDownstreamRoute(route);\n        await _next.Invoke(context);\n        return context;\n    }\n\n    /// <summary>\n    /// Processing the downstream routes with route keys except the main route that has already been processed.\n    /// </summary>\n    /// <param name=\"context\">The main http context.</param>\n    /// <param name=\"routes\">The downstream routes.</param>\n    /// <param name=\"routeKeysConfigs\">The route keys config.</param>\n    /// <param name=\"mainResponse\">The response from the main route.</param>\n    /// <returns>A list of the tasks' http contexts.</returns>\n    protected virtual async Task<HttpContext[]> ProcessRoutesWithRouteKeysAsync(HttpContext context, IEnumerable<DownstreamRoute> routes, IReadOnlyCollection<AggregateRouteConfig> routeKeysConfigs, HttpContext mainResponse)\n    {\n        var processing = new List<Task<HttpContext>>();\n        var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync();\n        var jObject = JToken.Parse(content);\n\n        foreach (var downstreamRoute in routes.Skip(1))\n        {\n            var matchAdvancedAgg = routeKeysConfigs.FirstOrDefault(q => q.RouteKey == downstreamRoute.Key);\n            if (matchAdvancedAgg != null)\n            {\n                processing.AddRange(ProcessRouteWithComplexAggregation(matchAdvancedAgg, jObject, context, downstreamRoute));\n                continue;\n            }\n\n            processing.Add(ProcessRouteAsync(context, downstreamRoute));\n        }\n\n        return await Task.WhenAll(processing);\n    }\n\n    /// <summary>\n    /// Mapping responses.\n    /// </summary>\n    private Task MapResponsesAsync(HttpContext context, Route route, HttpContext mainResponseContext, IEnumerable<HttpContext> responsesContexts)\n    {\n        var contexts = new List<HttpContext> { mainResponseContext };\n        contexts.AddRange(responsesContexts);\n        return MapAsync(context, route, contexts);\n    }\n\n    /// <summary>\n    /// Processing a route with aggregation.\n    /// </summary>\n    private IEnumerable<Task<HttpContext>> ProcessRouteWithComplexAggregation(AggregateRouteConfig matchAdvancedAgg,\n        JToken jObject, HttpContext httpContext, DownstreamRoute downstreamRoute)\n    {\n        var processing = new List<Task<HttpContext>>();\n        var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct();\n        foreach (var value in values)\n        {\n            var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues();\n            tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value));\n            processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv));\n        }\n\n        return processing;\n    }\n\n    /// <summary>\n    /// Process a downstream route asynchronously.\n    /// </summary>\n    /// <returns>The cloned Http context.</returns>\n    private async Task<HttpContext> ProcessRouteAsync(HttpContext sourceContext, DownstreamRoute route, List<PlaceholderNameAndValue> placeholders = null)\n    {\n        var newHttpContext = await CreateThreadContextAsync(sourceContext, route);\n        CopyItemsToNewContext(newHttpContext, sourceContext, placeholders);\n        newHttpContext.Items.UpsertDownstreamRoute(route);\n\n        await _next.Invoke(newHttpContext);\n        return newHttpContext;\n    }\n\n    /// <summary>\n    /// Copying some needed parameters to the Http context items.\n    /// </summary>\n    private static void CopyItemsToNewContext(HttpContext target, HttpContext source, List<PlaceholderNameAndValue> placeholders = null)\n    {\n        target.Items.Add(RequestIdString, source.Items[RequestIdString]);\n        target.Items.SetIInternalConfiguration(source.Items.IInternalConfiguration());\n        target.Items.UpsertTemplatePlaceholderNameAndValues(placeholders ??\n                                                            source.Items.TemplatePlaceholderNameAndValues());\n    }\r\n\r\n    /// <summary>\n    /// Creates a new HttpContext based on the source.\n    /// </summary>\n    /// <param name=\"source\">The base http context.</param>\r\n    /// <param name=\"route\">Downstream route.</param>\n    /// <returns>The cloned context.</returns>\n    protected virtual async Task<HttpContext> CreateThreadContextAsync(HttpContext source, DownstreamRoute route)\n    {\n        var from = source.Request;\r\n        var bodyStream = await CloneRequestBodyAsync(from, route, source.RequestAborted);\r\n        var target = new DefaultHttpContext\n        {\n            Request =\n            {\n                Body = bodyStream,\n                ContentLength = from.ContentLength,\n                ContentType = from.ContentType,\n                Host = from.Host,\n                Method = from.Method,\n                Path = from.Path,\n                PathBase = from.PathBase,\n                Protocol = from.Protocol,\n                QueryString = from.QueryString,\n                Scheme = from.Scheme,\n                IsHttps = from.IsHttps,\n                Query = new QueryCollection(new Dictionary<string, StringValues>(from.Query)),\n                RouteValues = new(from.RouteValues),\n            },\n            Connection =\n            {\n                RemoteIpAddress = source.Connection.RemoteIpAddress,\n            },\n            RequestServices = source.RequestServices,\n            RequestAborted = source.RequestAborted,\n            User = source.User,\n        };\n        foreach (var header in from.Headers)\n        {\n            target.Request.Headers[header.Key] = header.Value.ToArray();\n        }\r\n\r\n        // Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object\r\n        target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object\n        return target;\n    }\r\n\n    protected virtual Task MapAsync(HttpContext httpContext, Route route, List<HttpContext> contexts)\n    {\n        if (route.DownstreamRoute.Count == 1)\n        {\n            return Task.CompletedTask;\n        }\n\n        var aggregator = _factory.Get(route);\n        return aggregator.Aggregate(route, httpContext, contexts);\n    }\r\n\r\n    protected virtual async Task<Stream> CloneRequestBodyAsync(HttpRequest request, DownstreamRoute route, CancellationToken aborted)\r\n    {\r\n        request.EnableBuffering();\r\n        if (request.Body.Position != 0)\r\n        {\r\n            Logger.LogWarning(() => $\"Ocelot does not support body copy without stream in initial position 0 for the route {route.Name()}.\");\r\n            return request.Body;\r\n        }\r\n\r\n        var targetBuffer = new MemoryStream();\r\n        if (request.ContentLength is not null)\r\n        {\r\n            await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted);\r\n            targetBuffer.Position = 0;\r\n            request.Body.Position = 0;\r\n        }\r\n        else\r\n        {\r\n            Logger.LogInformation(() => $\"Aggregation does not support body copy without Content-Length header, skipping body copy for the route {route.Name()}.\");\r\n        }\r\n\r\n        return targetBuffer;\r\n    }\r\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.Multiplexer;\r\n\r\npublic class ServiceLocatorDefinedAggregatorProvider : IDefinedAggregatorProvider\r\n{\r\n    private readonly Dictionary<string, IDefinedAggregator> _aggregators;\r\n\r\n    public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services)\r\n    {\r\n        _aggregators = services.GetServices<IDefinedAggregator>().ToDictionary(x => x.GetType().Name);\r\n    }\r\n\r\n    public Response<IDefinedAggregator> Get(Route route)\r\n    {\r\n        if (_aggregators.TryGetValue(route.Aggregator, out var aggregator))\r\n        {\r\n            return new OkResponse<IDefinedAggregator>(aggregator);\r\n        }\r\n\r\n        return new ErrorResponse<IDefinedAggregator>(new CouldNotFindAggregatorError(route.Aggregator));\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/SimpleJsonResponseAggregator.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Middleware;\nusing System.Net.Http.Headers;\n\nnamespace Ocelot.Multiplexer;\n\npublic class SimpleJsonResponseAggregator : IResponseAggregator\n{\n    public async Task Aggregate(Route route, HttpContext originalContext, List<HttpContext> downstreamContexts)\n    {\n        await MapAggregateContent(originalContext, downstreamContexts);\n    }\n\n    private static async Task MapAggregateContent(HttpContext originalContext, List<HttpContext> downstreamContexts)\n    {\n        var contentBuilder = new StringBuilder();\n\n        contentBuilder.Append('{');\n\n        var responseKeys = downstreamContexts.Select(s => s.Items.DownstreamRoute().Key).Distinct().ToArray();\n\n        for (var k = 0; k < responseKeys.Length; k++)\n        {\n            var contexts = downstreamContexts.Where(w => w.Items.DownstreamRoute().Key == responseKeys[k]).ToArray();\n            if (contexts.Length == 1)\n            {\n                if (contexts[0].Items.Errors().Count > 0)\n                {\n                    MapAggregateError(originalContext, contexts[0]);\n                    return;\n                }\n\n                var content = await contexts[0].Items.DownstreamResponse().Content.ReadAsStringAsync();\n                contentBuilder.Append($\"\\\"{responseKeys[k]}\\\":{content}\");\n            }\n            else\n            {\n                contentBuilder.Append($\"\\\"{responseKeys[k]}\\\":\");\n                contentBuilder.Append('[');\n\n                for (var i = 0; i < contexts.Length; i++)\n                {\n                    if (contexts[i].Items.Errors().Count > 0)\n                    {\n                        MapAggregateError(originalContext, contexts[i]);\n                        return;\n                    }\n\n                    var content = await contexts[i].Items.DownstreamResponse().Content.ReadAsStringAsync();\n                    if (string.IsNullOrWhiteSpace(content))\n                    {\n                        continue;\n                    }\n\n                    contentBuilder.Append($\"{content}\");\n\n                    if (i + 1 < contexts.Length)\n                    {\n                        contentBuilder.Append(',');\n                    }\n                }\n\n                contentBuilder.Append(']');\n            }\n\n            if (k + 1 < responseKeys.Length)\n            {\n                contentBuilder.Append(',');\n            }\n        }\n\n        contentBuilder.Append('}');\n\n        var stringContent = new StringContent(contentBuilder.ToString())\n        {\n            Headers = { ContentType = new MediaTypeHeaderValue(\"application/json\") },\n        };\n\n        originalContext.Items.UpsertDownstreamResponse(new DownstreamResponse(stringContent, HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"cannot return from aggregate..which reason phrase would you use?\"));\n    }\n\n    private static void MapAggregateError(HttpContext originalContext, HttpContext downstreamContext)\n    {\n        originalContext.Items.UpsertErrors(downstreamContext.Items.Errors());\n        originalContext.Items.UpsertDownstreamResponse(downstreamContext.Items.DownstreamResponse());\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Multiplexer/UserDefinedResponseAggregator.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Multiplexer;\n\npublic class UserDefinedResponseAggregator : IResponseAggregator\n{\n    private readonly IDefinedAggregatorProvider _provider;\n\n    public UserDefinedResponseAggregator(IDefinedAggregatorProvider provider)\n    {\n        _provider = provider;\n    }\n\n    public async Task Aggregate(Route route, HttpContext originalContext, List<HttpContext> downstreamResponses)\n    {\n        var aggregator = _provider.Get(route);\n\n        if (!aggregator.IsError)\n        {\n            var aggregateResponse = await aggregator.Data\n                .Aggregate(downstreamResponses);\n\n            originalContext.Items.UpsertDownstreamResponse(aggregateResponse);\n        }\n        else\n        {\n            originalContext.Items.UpsertErrors(aggregator.Errors);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Ocelot.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <NoPackageAnalysis>true</NoPackageAnalysis>\n    <Description>Ocelot is an API gateway based on .NET stack.</Description>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <ProductName>Ocelot Gateway</ProductName>\n    <PackageId>Ocelot</PackageId>\n    <PackageTags>API Gateway;.NET Core;.NET</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>\n    <PackageIconUrl>https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png</PackageIconUrl>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <NoWarn>1591</NoWarn>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageReleaseNotes>https://github.com/ThreeMammals/Ocelot/blob/main/ReleaseNotes.md</PackageReleaseNotes>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n    <IncludeSymbols>True</IncludeSymbols>\n    <SymbolPackageFormat>snupkg</SymbolPackageFormat>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <DebugType>full</DebugType>\n    <DebugSymbols>True</DebugSymbols>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0|AnyCPU'\">\n    <WarningLevel>8</WarningLevel>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0|AnyCPU'\">\n    <WarningLevel>8</WarningLevel>\n  </PropertyGroup>\n  <!-- Project dependencies -->\n  <ItemGroup>\n    <PackageReference Include=\"FluentValidation\" Version=\"12.1.1\" />\n    <PackageReference Include=\"IPAddressRange\" Version=\"6.3.0\" />\n    <PackageReference Include=\"Microsoft.Extensions.DiagnosticAdapter\" Version=\"3.1.32\">\n      <NoWarn>NU1701</NoWarn>\n    </PackageReference>\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 8.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net8.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.MiddlewareAnalysis\" Version=\"8.0.25\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"8.0.25\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 9.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net9.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.MiddlewareAnalysis\" Version=\"9.0.14\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"9.0.14\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 10.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net10.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.MiddlewareAnalysis\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"10.0.5\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"..\\..\\README.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\README.md\" />\n    <None Include=\"..\\..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ocelot/QualityOfService/IQosFactory.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.QualityOfService;\n\npublic interface IQoSFactory\n{\n    Response<DelegatingHandler> Get(DownstreamRoute request);\n}\n"
  },
  {
    "path": "src/Ocelot/QualityOfService/NoQosDelegatingHandler.cs",
    "content": "namespace Ocelot.QualityOfService;\n\npublic class NoQosDelegatingHandler : DelegatingHandler\n{\n}\n"
  },
  {
    "path": "src/Ocelot/QualityOfService/QosDelegatingHandlerDelegate.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\n\nnamespace Ocelot.QualityOfService;\n\npublic delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory);\n"
  },
  {
    "path": "src/Ocelot/QualityOfService/QosFactory.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\n\nnamespace Ocelot.QualityOfService;\n\npublic class QoSFactory : IQoSFactory\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly IOcelotLoggerFactory _ocelotLoggerFactory;\n    private readonly IHttpContextAccessor _contextAccessor;\n\n    public QoSFactory(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory ocelotLoggerFactory)\n    {\n        _serviceProvider = serviceProvider;\n        _ocelotLoggerFactory = ocelotLoggerFactory;\n        _contextAccessor = contextAccessor;\n    }\n\n    public Response<DelegatingHandler> Get(DownstreamRoute request)\n    {\n        var handler = _serviceProvider.GetService<QosDelegatingHandlerDelegate>();\n\n        if (handler != null)\n        {\n            return new OkResponse<DelegatingHandler>(handler(request, _contextAccessor, _ocelotLoggerFactory));\n        }\n\n        return new ErrorResponse<DelegatingHandler>(new UnableToFindQoSProviderError($\"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}\"));\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/QualityOfService/UnableToFindQoSProviderError.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.QualityOfService;\n\npublic class UnableToFindQoSProviderError : Error\n{\n    public UnableToFindQoSProviderError(string message)\n        : base(message, OcelotErrorCode.UnableToFindQoSProviderError, StatusCodes.Status404NotFound)\n    { }\n}\n"
  },
  {
    "path": "src/Ocelot/QueryStrings/AddQueriesToRequest.cs",
    "content": "﻿using Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration;\r\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Request.Middleware;\r\nusing Ocelot.Responses;\nusing System.Security.Claims;\r\n\r\nnamespace Ocelot.QueryStrings;\r\n\r\npublic class AddQueriesToRequest : IAddQueriesToRequest\r\n{\r\n    private readonly IClaimsParser _claimsParser;\r\n\r\n    public AddQueriesToRequest(IClaimsParser claimsParser)\r\n    {\r\n        _claimsParser = claimsParser;\r\n    }\r\n\r\n    public Response SetQueriesOnDownstreamRequest(List<ClaimToThing> claimsToThings, IEnumerable<Claim> claims, DownstreamRequest downstreamRequest)\r\n    {\r\n        var queryDictionary = ConvertQueryStringToDictionary(downstreamRequest.Query);\r\n\r\n        foreach (var config in claimsToThings)\r\n        {\r\n            var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index);\r\n\r\n            if (value.IsError)\r\n            {\r\n                return new ErrorResponse(value.Errors);\r\n            }\r\n\r\n            var exists = queryDictionary.FirstOrDefault(x => x.Key == config.ExistingKey);\r\n\r\n            if (!string.IsNullOrEmpty(exists.Key))\r\n            {\r\n                queryDictionary[exists.Key] = value.Data;\r\n            }\r\n            else\r\n            {\r\n                queryDictionary.Add(config.ExistingKey, value.Data);\r\n            }\r\n        }\r\n\r\n        downstreamRequest.Query = ConvertDictionaryToQueryString(queryDictionary);\r\n\r\n        return new OkResponse();\r\n    }\r\n\r\n    private static Dictionary<string, StringValues> ConvertQueryStringToDictionary(string queryString)\r\n    {\r\n        var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers\r\n            .ParseQuery(queryString);\r\n\r\n        return query;\r\n    }\r\n\r\n    private static string ConvertDictionaryToQueryString(Dictionary<string, StringValues> queryDictionary)\r\n    {\r\n        var builder = new StringBuilder();\r\n\r\n        builder.Append('?');\n\n        var outerCount = 0;\r\n\r\n        foreach (var query in queryDictionary)\r\n        {\r\n            for (var innerCount = 0; innerCount < query.Value.Count; innerCount++)\r\n            {\r\n                builder.Append($\"{query.Key}={query.Value[innerCount]}\");\r\n\r\n                if (innerCount < (query.Value.Count - 1))\r\n                {\r\n                    builder.Append('&');\r\n                }\r\n            }\r\n\r\n            if (outerCount < (queryDictionary.Count - 1))\r\n            {\r\n                builder.Append('&');\r\n            }\r\n\r\n            outerCount++;\r\n        }\r\n\r\n        return builder.ToString();\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/QueryStrings/ClaimsToQueryStringMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.QueryStrings;\n\npublic class ClaimsToQueryStringMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IAddQueriesToRequest _addQueriesToRequest;\n\n    public ClaimsToQueryStringMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IAddQueriesToRequest addQueriesToRequest)\n            : base(loggerFactory.CreateLogger<ClaimsToQueryStringMiddleware>())\n    {\n        _next = next;\n        _addQueriesToRequest = addQueriesToRequest;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n\n        if (downstreamRoute.ClaimsToQueries.Any())\n        {\n            Logger.LogInformation(() => $\"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries\");\n\n            var downstreamRequest = httpContext.Items.DownstreamRequest();\n\n            var response = _addQueriesToRequest.SetQueriesOnDownstreamRequest(downstreamRoute.ClaimsToQueries, httpContext.User.Claims, downstreamRequest);\n\n            if (response.IsError)\n            {\n                Logger.LogWarning(\"there was an error setting queries on context, setting pipeline error\");\n\n                httpContext.Items.UpsertErrors(response.Errors);\n                return;\n            }\n        }\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/QueryStrings/IAddQueriesToRequest.cs",
    "content": "﻿using Ocelot.Configuration;\r\nusing Ocelot.Request.Middleware;\r\nusing Ocelot.Responses;\nusing System.Security.Claims;\r\n\r\nnamespace Ocelot.QueryStrings;\r\n\r\npublic interface IAddQueriesToRequest\r\n{\r\n    Response SetQueriesOnDownstreamRequest(List<ClaimToThing> claimsToThings, IEnumerable<Claim> claims, DownstreamRequest downstreamRequest);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/ClientRequestIdentity.cs",
    "content": "﻿namespace Ocelot.RateLimiting;\n\npublic readonly record struct ClientRequestIdentity(string ClientId, string LoadBalancerKey)\n{\n    public override string ToString() => $\"{ClientId}:{LoadBalancerKey}\";\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Distributed;\nusing Newtonsoft.Json;\n\nnamespace Ocelot.RateLimiting;\n\n/// <summary>\n/// Custom storage based on a distributed cache of a remote/local services.\n/// </summary>\n/// <remarks>\n/// See the <see cref=\"IDistributedCache\"/> interface docs for more details.\n/// </remarks>\npublic class DistributedCacheRateLimitStorage : IRateLimitStorage\n{\n    private readonly IDistributedCache _memoryCache;\n\n    public DistributedCacheRateLimitStorage(IDistributedCache memoryCache) => _memoryCache = memoryCache;\n\n    public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)\n        => _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime));\n\n    public bool Exists(string id) => !string.IsNullOrEmpty(_memoryCache.GetString(id));\n\n    public RateLimitCounter? Get(string id)\n    {\n        var stored = _memoryCache.GetString(id);\n        return string.IsNullOrEmpty(stored) ? null :\n            JsonConvert.DeserializeObject<RateLimitCounter>(stored);\n    }\n\n    public void Remove(string id) => _memoryCache.Remove(id);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/IRateLimitStorage.cs",
    "content": "﻿namespace Ocelot.RateLimiting;\n\n/// <summary>\n/// Defines a storage for keeping of rate limiting data.\n/// </summary>\n/// <remarks>Concrete classes should be based on solutions with excellent performance, such as in-memory solutions.</remarks>\npublic interface IRateLimitStorage\n{\n    bool Exists(string id);\n\n    RateLimitCounter? Get(string id);\n\n    void Remove(string id);\n\n    void Set(string id, RateLimitCounter counter, TimeSpan expirationTime);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/IRateLimiting.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\n\nnamespace Ocelot.RateLimiting;\n\n/// <summary>\n/// Defines basic Rate Limiting functionality.\n/// </summary>\npublic interface IRateLimiting\n{\n    /// <summary>Retrieves the key for the attached storage.</summary>\n    /// <remarks>See the <see cref=\"IRateLimitStorage\"/> interface.</remarks>\n    /// <param name=\"identity\">The current representation of the request.</param>\n    /// <param name=\"options\">The options of rate limiting.</param>\n    /// <returns>A <see langword=\"string\"/> value of the key.</returns>\n    string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options);\n\n    /// <summary>\n    /// Gets required information to create wanted headers in upper contexts (middleware, etc).\n    /// </summary>\n    /// <param name=\"context\">The current context.</param>\n    /// <param name=\"options\">The options of rate limiting.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <param name=\"counter\">The counter data.</param>\n    /// <returns>A <see cref=\"RateLimitHeaders\"/> value.</returns>\n    RateLimitHeaders GetHeaders(HttpContext context, RateLimitOptions options, DateTime now, RateLimitCounter counter);\n\n    /// <summary>\n    /// Main entry point to process the current request and apply the limiting rule.\n    /// </summary>\n    /// <remarks>Warning! The method performs the storage operations which should be thread safe.</remarks>\n    /// <param name=\"identity\">The representation of current request.</param>\n    /// <param name=\"options\">The current rate limiting options.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>A <see cref=\"RateLimitCounter\"/> value.</returns>\n    RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options, DateTime now);\n\n    /// <summary>\n    /// Counts requests based on the current counter state and taking into account the limiting rule.\n    /// </summary>\n    /// <param name=\"entry\">Old counter with starting moment inside.</param>\n    /// <param name=\"rule\">The limiting rule.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>A <see cref=\"RateLimitCounter\"/> value.</returns>\n    RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule, DateTime now);\n\n    /// <summary>\n    /// Gets the seconds to wait for the next retry by starting moment and the rule.\n    /// </summary>\n    /// <remarks>The method must be called after the counting by the <see cref=\"Count(RateLimitCounter?, RateLimitRule, DateTime)\"/> method is completed; otherwise it doesn't make sense.</remarks>\n    /// <param name=\"counter\">The counter with starting moment inside.</param>\n    /// <param name=\"rule\">The limiting rule.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>A <see cref=\"double\"/> value in seconds.</returns>\n    double RetryAfter(RateLimitCounter counter, RateLimitRule rule, DateTime now);\n\n    /// <summary>\n    /// Converts to time span from a string, such as \"1ms\", \"1s\", \"1m\", \"1h\", \"1d\".\n    /// </summary>\n    /// <param name=\"timespan\">The string value with units: '1ms', '1s', '1m', '1h', '1d'.</param>\n    /// <returns>A <see cref=\"TimeSpan\"/> value.</returns>\n    TimeSpan ToTimespan(string timespan);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Memory;\n\nnamespace Ocelot.RateLimiting;\n\n/// <summary>\n/// Default storage based on the memory cache of the local web server instance.\n/// </summary>\n/// <remarks>\n/// See the <see cref=\"IMemoryCache\"/> interface docs for more details.\n/// </remarks>\npublic class MemoryCacheRateLimitStorage : IRateLimitStorage\n{\n    private readonly IMemoryCache _memoryCache;\n\n    public MemoryCacheRateLimitStorage(IMemoryCache memoryCache) => _memoryCache = memoryCache;\n\n    public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)\n        => _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime));\n\n    public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter);\n\n    public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null;\n\n    public void Remove(string id) => _memoryCache.Remove(id);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/QuotaExceededError.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.RateLimiting;\n\npublic class QuotaExceededError : Error\n{\n    public QuotaExceededError(string message, int httpStatusCode)\n        : base(message, OcelotErrorCode.QuotaExceededError, httpStatusCode)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/RateLimitCounter.cs",
    "content": "﻿using System.Globalization;\nusing System.Text.Json.Serialization;\nusing NewtonsoftJsonConstructor = Newtonsoft.Json.JsonConstructorAttribute;\n\nnamespace Ocelot.RateLimiting;\n\n/// <summary>\n/// Stores the initial access time and the numbers of calls made from that point.\n/// </summary>\npublic struct RateLimitCounter\n{\n    public RateLimitCounter(DateTime startedAt)\n    {\n        StartedAt = startedAt;\n        Total = 1;\n    }\n\n    [JsonConstructor]\n    [NewtonsoftJsonConstructor]\n    public RateLimitCounter(DateTime startedAt, DateTime? exceededAt, long totalRequests)\n    {\n        StartedAt = startedAt;\n        ExceededAt = exceededAt;\n        Total = totalRequests;\n    }\n\n    /// <summary>The moment when the counting was started.</summary>\n    /// <value>A <see cref=\"DateTime\"/> value of the moment.</value>\n    public DateTime StartedAt { get; }\n\n    /// <summary>The moment when the limit was exceeded.</summary>\n    /// <value>A <see cref=\"DateTime\"/> value of the moment.</value>\n    public DateTime? ExceededAt;\n\n    /// <summary>Total number of requests counted.</summary>\n    /// <value>A <see langword=\"long\"/> value of total number.</value>\n    public long Total;\n\n    public override readonly string ToString()\n    {\n        string started = StartedAt.ToString(\"O\", CultureInfo.InvariantCulture);\n        string exceeded = ExceededAt.HasValue\n            ? $\"+{ExceededAt.Value - StartedAt}\"\n            : string.Empty;\n        return $\"{Total}->({started}){exceeded}\";\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/RateLimitHeaders.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.RateLimiting;\n\npublic class RateLimitHeaders\n{\n    protected RateLimitHeaders() { }\n\n    public RateLimitHeaders(HttpContext context, long limit, long remaining, DateTime reset)\n    {\n        Context = context;\n        Limit = limit;\n        Remaining = remaining;\n        Reset = reset;\n    }\n\n    /// <summary>\n    /// Original context.\n    /// </summary>\n    /// <value>An <see cref=\"HttpContext\"/> object.</value>\n    public HttpContext Context { get; }\n\n    /// <summary>\n    /// Total number of requests allowed in the current time window.\n    /// </summary>\n    /// <value>An <see cref=\"long\"/> value.</value>\n    public long Limit { get; }\n\n    /// <summary>\n    /// Number of requests remaining before hitting the limit.\n    /// </summary>\n    /// <value>An <see cref=\"long\"/> value.</value>\n    public long Remaining { get; }\n\n    /// <summary>\n    /// Timestamp when the rate limit window resets.\n    /// </summary>\n    /// <value>A <see cref=\"DateTime\"/> value.</value>\n    public DateTime Reset { get; }\n\n    public override string ToString() => $\"{Remaining}/{Limit} resets at {Reset:O}\";\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/RateLimiting.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.Extensions;\nusing System.Security.Cryptography;\n\nnamespace Ocelot.RateLimiting;\n\npublic class RateLimiting : IRateLimiting\n{\n    private readonly IRateLimitStorage _storage;\n#pragma warning disable IDE0079 // Remove unnecessary suppression\n#pragma warning disable IDE0330 // Prefer 'System.Threading.Lock'\n    private static readonly object ProcessLocker = new();\n    private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1);\n\n    public RateLimiting(IRateLimitStorage storage)\n    {\n        _storage = storage;\n    }\n\n    /// <summary>\n    /// Main entry point to process the current request and apply the limiting rule.\n    /// </summary>\n    /// <remarks>Warning! The method performs the storage operations which MUST BE thread safe.</remarks>\n    /// <param name=\"identity\">The representation of current request.</param>\n    /// <param name=\"options\">The current rate limiting options.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>A <see cref=\"RateLimitCounter\"/> value.</returns>\n    public virtual RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options, DateTime now)\n    {\n        RateLimitCounter counter;\n        var rule = options.Rule;\n        var counterId = GetStorageKey(identity, options);\n\n        // Serial reads/writes from/to the storage which must be thread safe\n        lock (ProcessLocker)\n        {\n            var entry = _storage.Get(counterId);\n            counter = Count(entry, rule, now);\n            if (counter.Total > rule.Limit)\n            {\n                var retryAfter = RetryAfter(counter, rule, now); // the calculation depends on the counter returned from Count() method\n                if (retryAfter < 0)\n                {\n                    // Wait window period elapsed, reset counter, and start the next counting period\n                    counter = new RateLimitCounter(now);\n                }\n            }\n\n            // TODO: The expiry approach doesn't make much sense in practice because\n            // if the counting period elapses or there are prolonged pending periods,\n            // the counter resets to a state of 1, with null values after expiry treated as a count of 1.\n            // It might make sense to consider request timeout periods as expiry periods.\n            var expiration = rule.PeriodSpan + rule.WaitSpan; // absolute max period of processing\n            expiration += OneSecond; // add an extra second as a shift to allow to synchronize concurrent threads\n\n            _storage.Set(counterId, counter, expiration);\n        }\n\n        return counter;\n    }\n\n    /// <summary>\n    /// Counts requests based on the current counter state and taking into account the limiting rule.\n    /// </summary>\n    /// <param name=\"entry\">Old counter with starting moment inside.</param>\n    /// <param name=\"rule\">The limiting rule.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>A <see cref=\"RateLimitCounter\"/> value.</returns>\n    public virtual RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule, DateTime now)\n    {\n        if (!entry.HasValue)\n        {\n            return new RateLimitCounter(now); // no entry, start counting, and the current request is the 1st one\n        }\n\n        var counter = entry.Value;\n        if (++counter.Total > rule.Limit && !counter.ExceededAt.HasValue) // current request exceeds the limit\n        {\n            counter.ExceededAt = now; // the exceeding moment is now, this request should fail\n        }\n\n        bool isInFixedWindow = counter.StartedAt + rule.PeriodSpan >= now; // the fixed window counting period\n        bool isInWaitWindow = counter.ExceededAt.HasValue && counter.ExceededAt.Value + rule.WaitSpan >= now; // with including equality, treating the end of waiting as the end of the wait window\n        return isInFixedWindow || isInWaitWindow\n            ? counter // still count\n            : new RateLimitCounter(now); // Wait window period elapsed, start counting NOW!\n    }\n\n    public virtual RateLimitHeaders GetHeaders(HttpContext context, RateLimitOptions options, DateTime now, RateLimitCounter counter)\n    {\n        var rule = options.Rule;\n        return new RateLimitHeaders(context,\n            limit: rule.Limit,\n            remaining: rule.Limit - counter.Total,\n            reset: counter.StartedAt + rule.PeriodSpan);\n    }\n\n    /// <summary>\n    /// Gets the SHA1-hashed value of a unique key for caching, using the <see cref=\"IMemoryCache\"/> service through the <see cref=\"IRateLimitStorage\"/> service.\n    /// </summary>\n    /// <remarks>Notes:<list type=\"bullet\">\n    /// <item>The generated identity key includes the <see cref=\"RateLimitOptions.KeyPrefix\"/> as a prefix to ensure it is recognized in distributed storage systems, like <see cref=\"IDistributedCache\"/> services, aiding users in observing/managing cached objects.\n    /// By default, each Ocelot instance employs its own <see cref=\"IMemoryCache\"/> service, without synchronization across instances.</item>\n    /// </list></remarks>\n    /// <param name=\"identity\">Specifies the client's identity.</param>\n    /// <param name=\"options\">Defines the current route rate-limiting options.</param>\n    /// <returns>Returns a SHA1-hashed <see cref=\"string\"/> object as the caching key.</returns>\n    public virtual string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options)\n    {\n        var key = $\"{options.KeyPrefix}_{identity}_{options.Rule}\";\n        var idBytes = Encoding.UTF8.GetBytes(key);\n        var hashBytes = SHA1.HashData(idBytes);\n        return Convert.ToHexString(hashBytes);\n    }\n\n    /// <summary>\n    /// Gets the seconds to wait for the next retry by starting moment and the rule.\n    /// </summary>\n    /// <remarks>The method must be called after the <see cref=\"Count(RateLimitCounter?, RateLimitRule, DateTime)\"/> one.</remarks>\n    /// <param name=\"counter\">The counter state.</param>\n    /// <param name=\"rule\">The current rule.</param>\n    /// <param name=\"now\">The processing moment.</param>\n    /// <returns>An <see cref=\"int\"/> value of seconds.</returns>\n    public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule, DateTime now)\n    {\n        if (counter.Total <= rule.Limit || !counter.ExceededAt.HasValue)\n        {\n            return 0.0D; // happy path, no need to retry, current request is valid, continue counting\n        }\n\n        // Counting Period is active\n        bool doNotWait = rule.WaitSpan == TimeSpan.Zero || rule.Wait.IsEmpty() || rule.Wait == RateLimitRule.ZeroWait;\n        if (doNotWait && counter.StartedAt + rule.PeriodSpan > now)\n        {\n            //return waitWindow.TotalSeconds - (now - exceededAt).TotalSeconds; // minus seconds past\n            var retryAfter = counter.StartedAt + rule.PeriodSpan - now;\n            return retryAfter.TotalSeconds; // positive value of seconds until the end of the sliding period in fixed window\n        }\n\n        // Exceeding was happen && Wait period is active (no sliding)\n        var waitWindow = rule.WaitSpan; // good non-zero value\n        var exceededAt = counter.ExceededAt.Value;\n        if (exceededAt + waitWindow > now)\n        {\n            var retryAfter = exceededAt + waitWindow - now;\n            return retryAfter.TotalSeconds; // positive value of seconds until the end of the waiting period\n        }\n\n        return -1.0D; // counting period vs wait period elapsed, no need to retry, reset the counter in upper calling context\n    }\n\n    /// <summary>\n    /// Converts to time span from a string, such as \"1ms\", \"1s\", \"1m\", \"1h\", \"1d\".\n    /// </summary>\n    /// <param name=\"timespan\">The string value with units: '1ms', '1s', '1m', '1h', '1d'.</param>\n    /// <returns>A <see cref=\"TimeSpan\"/> value.</returns>\n    /// <exception cref=\"FormatException\">See more in the <see cref=\"RateLimitRule.ParseTimespan(string)\"/> method docs.</exception>\n    public virtual TimeSpan ToTimespan(string timespan) => RateLimitRule.ParseTimespan(timespan);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/RateLimitingHeaders.cs",
    "content": "﻿using Microsoft.Net.Http.Headers;\n\nnamespace Ocelot.RateLimiting;\n\n/// <summary>\n/// TODO These Ocelot's RateLimiting headers don't follow industry standards, see links.\n/// </summary>\n/// <remarks>Links:\n/// <list type=\"bullet\">\n/// <item>GitHub: <see href=\"https://github.com/ioggstream/draft-polli-ratelimit-headers\">draft-polli-ratelimit-headers</see></item>\n/// <item>GitHub: <see href=\"https://github.com/ietf-wg-httpapi/ratelimit-headers\">ratelimit-headers</see></item>\n/// <item>GitHub Wiki: <see href=\"https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html\">RateLimit header fields for HTTP</see></item>\n/// <item>StackOverflow: <see href=\"https://stackoverflow.com/questions/16022624/examples-of-http-api-rate-limiting-http-response-headers\">Examples of HTTP API Rate Limiting HTTP Response headers</see></item>\n/// </list>\n/// </remarks>\npublic static class RateLimitingHeaders\n{\n    public const char Dash = '-';\n    public const char Underscore = '_';\n\n    /// <summary>Gets the <c>Retry-After</c> HTTP header name.</summary>\n    public static readonly string Retry_After = HeaderNames.RetryAfter;\n\n    /// <summary>Gets the <c>X-RateLimit-Limit</c> Ocelot's header name.</summary>\n    public static readonly string X_RateLimit_Limit = nameof(X_RateLimit_Limit).Replace(Underscore, Dash);\n\n    /// <summary>Gets the <c>X-RateLimit-Remaining</c> Ocelot's header name.</summary>\n    public static readonly string X_RateLimit_Remaining = nameof(X_RateLimit_Remaining).Replace(Underscore, Dash);\n\n    /// <summary>Gets the <c>X-RateLimit-Reset</c> Ocelot's header name.</summary>\n    public static readonly string X_RateLimit_Reset = nameof(X_RateLimit_Reset).Replace(Underscore, Dash);\n}\n"
  },
  {
    "path": "src/Ocelot/RateLimiting/RateLimitingMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Globalization;\n\nnamespace Ocelot.RateLimiting;\n\npublic class RateLimitingMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IRateLimiting _limiter;\n    private readonly IHttpContextAccessor _contextAccessor;\n\n    public RateLimitingMiddleware(\n        RequestDelegate next,\n        IOcelotLoggerFactory factory,\n        IRateLimiting limiter,\n        IHttpContextAccessor contextAccessor)\n        : base(factory.CreateLogger<RateLimitingMiddleware>())\n    {\n        _next = next;\n        _limiter = limiter;\n        _contextAccessor = contextAccessor;\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        var now = DateTime.UtcNow;\n        context.Items.Add(nameof(DateTime.UtcNow), now);\n        var downstreamRoute = context.Items.DownstreamRoute();\n        var options = downstreamRoute.RateLimitOptions ?? new(false);\n        if (!options.EnableRateLimiting)\n        {\n            Logger.LogInformation(() => $\"Rate limiting is disabled for route '{downstreamRoute.Name()}' via the {nameof(RateLimitOptions.EnableRateLimiting)} option.\");\n            return _next.Invoke(context);\n        }\n\n        var identity = Identify(context, options, downstreamRoute);\n        if (IsWhitelisted(identity, options))\n        {\n            Logger.LogInformation(() => $\"Route '{downstreamRoute.Name()}' is configured to bypass rate limiting based on the client's header, due to the client's ID being detected in the whitelist.\");\n            return _next.Invoke(context);\n        }\n\n        // Log warnings and break execution\n        var rule = options.Rule;\n        var warning = string.Empty;\n        if (identity.ClientId.IsEmpty()) // unknown client aka security check, so block unknown clients\n        {\n            warning = $\"Rate limiting client could not be identified for the route '{downstreamRoute.Name(true)}' due to a missing or unknown client ID header required by rule '{rule}'!\"; // and don't log the header name because of security\n        }\n\n        if (!warning.IsEmpty())\n        {\n            Logger.LogWarning(warning);\n            RateLimitOptions errorOpts = new(options)\n            {\n                QuotaMessage = warning,\n                StatusCode = StatusCodes.Status503ServiceUnavailable,\n            };\n            return Break(context, errorOpts, -1.0);\n        }\n\n        var counter = _limiter.ProcessRequest(identity, options, now);\n        if (counter.Total > rule.Limit)\n        {\n            var retryAfter = _limiter.RetryAfter(counter, rule, now); // compute retry after value based on counter state\n            LogBlockedRequest(context, identity, counter, options, downstreamRoute); // log blocked request virtually\n            return Break(context, options, retryAfter);\n        }\n\n        // Set X-RateLimit-* headers for the longest period\n        var originalContext = _contextAccessor.HttpContext;\n        if (options.EnableHeaders && originalContext != null)\n        {\n            var headers = _limiter.GetHeaders(originalContext, options, now, counter);\n            originalContext.Response.OnStarting(SetRateLimitHeaders, state: headers);\n            Logger.LogInformation(() => $\"Route '{downstreamRoute.Name()}' must return rate limiting headers with the following data: {headers}\");\n        }\n\n        return _next.Invoke(context);\n    }\n\n    protected virtual Task Break(HttpContext context, RateLimitOptions options, double retryAfter)\n    {\n        var retryAfterHeader = retryAfter.ToString(CultureInfo.InvariantCulture);\n        var ds = ReturnQuotaExceededResponse(context, options, retryAfterHeader);\n        context.Items.UpsertDownstreamResponse(ds);\n        var error = new QuotaExceededError(GetResponseMessage(options), options.StatusCode);\n        context.Items.SetError(error);\n        return Task.CompletedTask;\n    }\n\n    protected virtual ClientRequestIdentity Identify(HttpContext context, RateLimitOptions options, DownstreamRoute route)\n    {\n        var clientId = string.Empty;\n        var header = options.ClientIdHeader.IfEmpty(RateLimitOptions.DefaultClientHeader);\n        if (context.Request.Headers.TryGetValue(header, out var headerValue))\n        {\n            clientId = headerValue;\n        }\n\n        return new ClientRequestIdentity(clientId, route.LoadBalancerKey);\n    }\n\n    public static bool IsWhitelisted(ClientRequestIdentity identity, RateLimitOptions options)\n        => options.ClientWhitelist.Contains(identity.ClientId);\n\n    public virtual void LogBlockedRequest(HttpContext context, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitOptions options, DownstreamRoute route)\n    {\n        var req = context.Request;\n        var rule = options.Rule;\n        Logger.LogWarning(() =>\n            $\"Blocked request: {req.Method} {req.Path} from client with {options.ClientIdHeader}({identity.ClientId}) header, quota {rule.Limit}/{rule.Period} exceeded by {counter.Total}. Blocked by rate limiting rule '{rule}' of the route '{route.Name()}' with {nameof(HttpContext.TraceIdentifier)}:{context.TraceIdentifier}.\");\n    }\n\n    public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext context, RateLimitOptions options, string retryAfter)\n    {\n        var message = GetResponseMessage(options);\n        var http = new HttpResponseMessage((HttpStatusCode)options.StatusCode)\n        {\n            Content = new StringContent(message),\n        };\n\n        if (options.EnableHeaders)\n        {\n            http.Headers.TryAddWithoutValidation(HeaderNames.RetryAfter, retryAfter); // in seconds, not date string\n            context.Response.Headers.RetryAfter = retryAfter;\n        }\n\n        return new DownstreamResponse(http);\n    }\n\n    protected virtual string GetResponseMessage(RateLimitOptions options)\n    {\n        var format = options.QuotaMessage.IfEmpty(RateLimitOptions.DefaultQuotaMessage);\n        return string.Format(format, options.Rule.Limit, options.Rule.Period);\n    }\n\n    /// <summary>TODO: Produced Ocelot's headers don't follow industry standards.</summary>\n    /// <remarks>More details in <see cref=\"RateLimitingHeaders\"/> docs.</remarks>\n    /// <param name=\"state\">Captured state as a <see cref=\"RateLimitHeaders\"/> object.</param>\n    /// <returns>A <see cref=\"Task.CompletedTask\"/> object.</returns>\n    protected virtual Task SetRateLimitHeaders(object state)\n    {\n        var limitHeaders = (RateLimitHeaders)state;\n        var headers = limitHeaders.Context.Response.Headers;\n        headers[RateLimitingHeaders.X_RateLimit_Limit] = new StringValues(limitHeaders.Limit.ToString());\n        headers[RateLimitingHeaders.X_RateLimit_Remaining] = new StringValues(limitHeaders.Remaining.ToString());\n        headers[RateLimitingHeaders.X_RateLimit_Reset] = new StringValues(limitHeaders.Reset.ToUniversalTime().ToString(\"o\", DateTimeFormatInfo.InvariantInfo));\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Creator/DownstreamRequestCreator.cs",
    "content": "using Ocelot.Infrastructure;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.Request.Creator;\n\npublic class DownstreamRequestCreator : IDownstreamRequestCreator\n{\n    private readonly IFrameworkDescription _framework;\n    private const string DotNetFramework = \".NET Framework\";\n\n    public DownstreamRequestCreator(IFrameworkDescription framework)\n    {\n        _framework = framework;\n    }\n\n    /// <summary>\n    /// According to https://tools.ietf.org/html/rfc7231\n    /// GET,HEAD,DELETE,CONNECT,TRACE\n    /// Can have body but server can reject the request.\n    /// And MS HttpClient in Full Framework actually rejects it.\n    /// See #366 issue.\n    /// </summary>\n    /// <param name=\"request\">The HTTP request.</param>\n    /// <returns>A <see cref=\"DownstreamRequest\"/> object.</returns>\n    public DownstreamRequest Create(HttpRequestMessage request)\n    {\n        if (_framework.Get().Contains(DotNetFramework))\n        {\n            if (request.Method == HttpMethod.Get ||\n                request.Method == HttpMethod.Head ||\n                request.Method == HttpMethod.Delete ||\n                request.Method == HttpMethod.Trace)\n            {\n                request.Content = null;\n            }\n        }\n\n        return new DownstreamRequest(request);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs",
    "content": "using Ocelot.Request.Middleware;\n\nnamespace Ocelot.Request.Creator;\n\npublic interface IDownstreamRequestCreator\n{\n    DownstreamRequest Create(HttpRequestMessage request);\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Mapper/IRequestMapper.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\n\nnamespace Ocelot.Request.Mapper;\n\npublic interface IRequestMapper\n{\n    HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute);\n}\r\n"
  },
  {
    "path": "src/Ocelot/Request/Mapper/PayloadTooLargeError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Request.Mapper;\n\npublic class PayloadTooLargeError : Error\n{\n    public PayloadTooLargeError(Exception exception) : base(exception.Message, OcelotErrorCode.PayloadTooLargeError, (int) System.Net.HttpStatusCode.RequestEntityTooLarge)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Mapper/RequestMapper.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Microsoft.AspNetCore.Http.Extensions;\r\nusing Microsoft.Extensions.Primitives;\r\nusing Ocelot.Configuration;\r\n\r\nnamespace Ocelot.Request.Mapper;\r\n\r\npublic class RequestMapper : IRequestMapper\r\n{\r\n    private static readonly HashSet<string> UnsupportedHeaders = new(StringComparer.OrdinalIgnoreCase) { \"host\", \"transfer-encoding\" };\r\n    private static readonly string[] ContentHeaders = { \"Content-Length\", \"Content-Language\", \"Content-Location\", \"Content-Range\", \"Content-MD5\", \"Content-Disposition\", \"Content-Encoding\" };\r\n\r\n    public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute)\r\n    {\r\n        var requestMessage = new HttpRequestMessage\r\n        {\r\n            Content = MapContent(request),\r\n            Method = MapMethod(request, downstreamRoute),\r\n            RequestUri = MapUri(request),\r\n            Version = downstreamRoute.DownstreamHttpVersion,\r\n            VersionPolicy = downstreamRoute.DownstreamHttpVersionPolicy,\r\n        };\r\n\r\n        MapHeaders(request, requestMessage);\r\n        return requestMessage;\r\n    }\r\n\r\n    private static HttpContent MapContent(HttpRequest request)\r\n    {\r\n        HttpContent content;\n\n        // No content if we have no body or if the request has no content according to RFC 2616 section 4.3\r\n        if (request.Body == null\r\n            || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding)))\r\n        {\r\n            return null;\r\n        }\r\n\r\n        content = request.ContentLength is 0\n            ? new ByteArrayContent(Array.Empty<byte>()) \n            : new StreamHttpContent(request.HttpContext);\r\n\n        AddContentHeaders(request, content);\r\n\r\n        return content;\r\n    }\r\n\r\n    private static void AddContentHeaders(HttpRequest request, HttpContent content)\r\n    {\r\n        if (!string.IsNullOrEmpty(request.ContentType))\r\n        {\r\n            content.Headers\r\n                .TryAddWithoutValidation(\"Content-Type\", new[] { request.ContentType });\r\n        }\r\n\r\n        // The performance might be improved by retrieving the matching headers from the request\r\n        // instead of calling request.Headers.TryGetValue for each used content header\r\n        var matchingHeaders = ContentHeaders.Where(request.Headers.ContainsKey);\r\n\r\n        foreach (var key in matchingHeaders)\r\n        {\r\n            if (!request.Headers.TryGetValue(key, out var value))\r\n            {\r\n                continue;\r\n            }\r\n\r\n            content.Headers.TryAddWithoutValidation(key, value.ToArray());\r\n        }\r\n    }\r\n\r\n    private static HttpMethod MapMethod(HttpRequest request, DownstreamRoute downstreamRoute) => \r\n        !string.IsNullOrEmpty(downstreamRoute?.DownstreamHttpMethod) ? \r\n            new HttpMethod(downstreamRoute.DownstreamHttpMethod) : new HttpMethod(request.Method);\r\n\r\n    // TODO Review this method, request.GetEncodedUrl() could throw a NullReferenceException\r\n    private static Uri MapUri(HttpRequest request) => new(request.GetEncodedUrl());\r\n\r\n    private static void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage)\r\n    {\r\n        foreach (var header in request.Headers)\r\n        {\r\n            if (IsSupportedHeader(header))\r\n            {\r\n                requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());\r\n            }\r\n        }\r\n    }\r\n\r\n    private static bool IsSupportedHeader(KeyValuePair<string, StringValues> header) =>\r\n        !UnsupportedHeaders.Contains(header.Key);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Request/Mapper/StreamHttpContent.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing System.Buffers;\n\nnamespace Ocelot.Request.Mapper;\n\npublic class StreamHttpContent : HttpContent\n{\n    private const int DefaultBufferSize = 65536;\n    public const long UnknownLength = -1;\n    private readonly HttpContext _context;\n    private readonly long _contentLength;\n\n    public StreamHttpContent(HttpContext context)\n    {\n        _context = context ?? throw new ArgumentNullException(nameof(context));\n        _contentLength = context.Request.ContentLength ?? UnknownLength;\n    }\n\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)\n        => CopyAsync(_context.Request.Body, stream, _contentLength, false, cancellationToken);\n\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)\n        => CopyAsync(_context.Request.Body, stream, _contentLength, false, CancellationToken.None);\n\n    protected override bool TryComputeLength(out long length)\n    {\n        length = _contentLength;\n        return length >= 0;\n    }\n\n    // This is used internally by HttpContent.ReadAsStreamAsync(...)\n    protected override Task<Stream> CreateContentReadStreamAsync()\n    {\n        // Nobody should be calling this...\n        throw new NotImplementedException();\n    }\n\n    private static async Task CopyAsync(Stream input, Stream output, long announcedContentLength,\n        bool autoFlush, CancellationToken cancellation)\n    {\n        // For smaller payloads, avoid allocating a buffer that is larger than the announced content length\n        var minBufferSize = announcedContentLength != UnknownLength && announcedContentLength < DefaultBufferSize\n            ? (int)announcedContentLength\n            : DefaultBufferSize;\n\n        var buffer = ArrayPool<byte>.Shared.Rent(minBufferSize);\n        long contentLength = 0;\n        try\n        {\n            while (true)\n            {\n                // Issue a zero-byte read to the input stream to defer buffer allocation until data is available.\n                // Note that if the underlying stream does not supporting blocking on zero byte reads, then this will\n                // complete immediately and won't save any memory, but will still function correctly.\n                var zeroByteReadTask = input.ReadAsync(Memory<byte>.Empty, cancellation);\n                if (zeroByteReadTask.IsCompletedSuccessfully)\n                {\n                    // Consume the ValueTask's result in case it is backed by an IValueTaskSource\n                    // It is save to read the Result once after the ValueTask has completed, and we've checked for complition by IsCompletedSuccessfully property\n                    // See remarks: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1.result?view=net-8.0#remarks\n                    _ = zeroByteReadTask.Result; // No need to await the task by .GetAwaiter().GetResult()\n                }\n                else\n                {\n                    // Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws\n                    var bufferToReturn = buffer;\n                    buffer = null;\n                    ArrayPool<byte>.Shared.Return(bufferToReturn);\n\n                    await zeroByteReadTask;\n\n                    buffer = ArrayPool<byte>.Shared.Rent(minBufferSize);\n                }\n\n                var read = await input.ReadAsync(buffer.AsMemory(), cancellation);\n                contentLength += read;\n\n                // Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body.\n                if (announcedContentLength != UnknownLength && contentLength > announcedContentLength)\n                {\n                    throw new InvalidOperationException($\"More data ({contentLength} bytes) received than the specified Content-Length of {announcedContentLength} bytes.\");\n                }\n\n                // End of the source stream.\n                if (read == 0)\n                {\n                    if (announcedContentLength == UnknownLength || contentLength == announcedContentLength)\n                    {\n                        return;\n                    }\n                    else\n                    {\n                        throw new InvalidOperationException($\"Sent {contentLength} request content bytes, but Content-Length promised {announcedContentLength}.\");\n                    }\n                }\n\n                await output.WriteAsync(buffer.AsMemory(0, read), cancellation);\n                if (autoFlush)\n                {\n                    // HttpClient doesn't always flush outgoing data unless the buffer is full or the caller asks.\n                    // This is a problem for streaming protocols like WebSockets and gRPC.\n                    await output.FlushAsync(cancellation);\n                }\n            }\n        }\n        finally\n        {\n            if (buffer != null)\n            {\n                ArrayPool<byte>.Shared.Return(buffer);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Mapper/UnmappableRequestError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Request.Mapper;\n\npublic class UnmappableRequestError : Error\n{\n    public UnmappableRequestError(Exception exception) : base($\"Error when parsing incoming request, exception: {exception}\", OcelotErrorCode.UnmappableRequestError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Middleware/DownstreamRequest.cs",
    "content": "using System.Net.Http.Headers;\n\nnamespace Ocelot.Request.Middleware;\n\npublic class DownstreamRequest\n{\n    private readonly HttpRequestMessage _request;\n\n    public DownstreamRequest() { }\n\n    public DownstreamRequest(HttpRequestMessage request)\n    {\n        _request = request;\n        Method = _request.Method.Method;\n        OriginalString = _request.RequestUri.OriginalString;\n        Scheme = _request.RequestUri.Scheme;\n        Host = _request.RequestUri.Host;\n        Port = _request.RequestUri.Port;\n        AbsolutePath = _request.RequestUri.AbsolutePath;\n        Query = _request.RequestUri.Query;\n    }\n\n    public HttpHeaders Headers { get => _request.Headers; }\n\n    public string Method { get; }\n\n    public string OriginalString { get; }\n\n    public string Scheme { get; set; }\n\n    public string Host { get; set; }\n\n    public int Port { get; set; }\n\n    public string AbsolutePath { get; set; }\n\n    public string Query { get; set; }\n\n    public bool HasContent { get => _request?.Content != null; }\n\n    public HttpRequestMessage Request { get => _request; }\n\n    public HttpRequestMessage ToHttpRequestMessage()\n    {\n        var uriBuilder = new UriBuilder\n        {\n            Port = Port,\n            Host = Host,\n            Path = AbsolutePath,\n            Query = RemoveLeadingQuestionMark(Query),\n            Scheme = Scheme,\n        };\n\n        _request.RequestUri = uriBuilder.Uri;\n        _request.Method = new HttpMethod(Method);\n        return _request;\n    }\n\n    public string ToUri()\n    {\n        var uriBuilder = new UriBuilder\n        {\n            Port = Port,\n            Host = Host,\n            Path = AbsolutePath,\n            Query = RemoveLeadingQuestionMark(Query),\n            Scheme = Scheme,\n        };\n\n        return uriBuilder.Uri.AbsoluteUri;\n    }\n\n    public override string ToString()\n    {\n        return ToUri();\n    }\n\n    private static string RemoveLeadingQuestionMark(string query)\n    {\n        if (!string.IsNullOrEmpty(query) && query.StartsWith('?'))\n        {\n            return query.Substring(1);\n        }\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Creator;\nusing Ocelot.Request.Mapper;\n\nnamespace Ocelot.Request.Middleware;\n\npublic class DownstreamRequestInitialiserMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IRequestMapper _requestMapper;\n    private readonly IDownstreamRequestCreator _creator;\n\n    public DownstreamRequestInitialiserMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IRequestMapper requestMapper,\n        IDownstreamRequestCreator creator)\n        : base(loggerFactory.CreateLogger<DownstreamRequestInitialiserMiddleware>())\n    {\n        _next = next;\n        _requestMapper = requestMapper;\n        _creator = creator;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n        HttpRequestMessage httpRequestMessage;\n\n        try\n        {\n            httpRequestMessage = _requestMapper.Map(httpContext.Request, downstreamRoute);\n        }\n        catch (Exception ex)\n        {\n            // TODO Review the error handling, we should throw an exception here and use the global error handler middleware to catch it\n            httpContext.Items.SetError(new UnmappableRequestError(ex));\n            return;\n        }\n\n        var downstreamRequest = _creator.Create(httpRequestMessage);\n        httpContext.Items.UpsertDownstreamRequest(downstreamRequest);\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/RequestId/DefaultRequestIdKey.cs",
    "content": "﻿namespace Ocelot.RequestId;\r\n\r\npublic static class DefaultRequestIdKey\r\n{\r\n    // This is set incase anyone isnt doing this specifically with there requests.\r\n    // It will not be forwarded on to downstream services unless specfied in the config.\r\n    public const string Value = \"RequestId\";\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing System.Net.Http.Headers;\n\nnamespace Ocelot.RequestId.Middleware;\n\npublic class RequestIdMiddleware : OcelotMiddleware\n{\n    public const string RequestIdName = nameof(IInternalConfiguration.RequestId);\n    public const string PreviousRequestIdName = \"Previous\" + nameof(IInternalConfiguration.RequestId);\n\n    private readonly RequestDelegate _next;\n    private readonly IRequestScopedDataRepository _requestScopedDataRepository;\n\n    public RequestIdMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IRequestScopedDataRepository requestScopedDataRepository)\n            : base(loggerFactory.CreateLogger<RequestIdMiddleware>())\n    {\n        _next = next;\n        _requestScopedDataRepository = requestScopedDataRepository;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        SetOcelotRequestId(httpContext);\n        await _next.Invoke(httpContext);\n    }\n\n    private void SetOcelotRequestId(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n\n        var key = downstreamRoute.RequestIdKey ?? DefaultRequestIdKey.Value;\n\n        if (httpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds))\n        {\n            httpContext.TraceIdentifier = upstreamRequestIds.First();\n\n            var previousRequestId = _requestScopedDataRepository.Get<string>(RequestIdName);\n            if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != httpContext.TraceIdentifier)\n            {\n                _requestScopedDataRepository.Add(PreviousRequestIdName, previousRequestId.Data);\n                _requestScopedDataRepository.Update(RequestIdName, httpContext.TraceIdentifier);\n            }\n            else\n            {\n                _requestScopedDataRepository.Add(RequestIdName, httpContext.TraceIdentifier);\n            }\n        }\n\n        var requestId = new RequestId(downstreamRoute.RequestIdKey, httpContext.TraceIdentifier);\n\n        var downstreamRequest = httpContext.Items.DownstreamRequest();\n\n        if (ShouldAddRequestId(requestId, downstreamRequest.Headers))\n        {\n            AddRequestIdHeader(requestId, downstreamRequest);\n        }\n    }\n\n    private static bool ShouldAddRequestId(RequestId requestId, HttpHeaders headers)\n    {\n        return !string.IsNullOrEmpty(requestId?.RequestIdKey)\n               && !string.IsNullOrEmpty(requestId.RequestIdValue)\n               && !RequestIdInHeaders(requestId, headers);\n    }\n\n    private static bool RequestIdInHeaders(RequestId requestId, HttpHeaders headers)\n    {\n        return headers.TryGetValues(requestId.RequestIdKey, out var value);\n    }\n\n    private static void AddRequestIdHeader(RequestId requestId, DownstreamRequest httpRequestMessage)\n    {\n        httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/RequestId/RequestId.cs",
    "content": "﻿namespace Ocelot.RequestId;\r\n\r\npublic class RequestId\r\n{\r\n    public RequestId(string requestIdKey, string requestIdValue)\r\n    {\r\n        RequestIdKey = requestIdKey;\r\n        RequestIdValue = requestIdValue;\r\n    }\r\n\r\n    public string RequestIdKey { get; }\r\n    public string RequestIdValue { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Requester/ConnectionToDownstreamServiceError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Requester;\n\npublic class ConnectionToDownstreamServiceError : Error\n{\n    public ConnectionToDownstreamServiceError(Exception exception)\n        : base($\"Error connecting to downstream service, exception: {exception}\", OcelotErrorCode.ConnectionToDownstreamServiceError, 502)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/DelegatingHandlerFactory.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.QualityOfService;\n\nnamespace Ocelot.Requester;\n\npublic class DelegatingHandlerFactory : IDelegatingHandlerFactory\n{\n    private readonly ITracingHandlerFactory _tracingFactory;\n    private readonly IQoSFactory _qoSFactory;\n    private readonly IServiceProvider _serviceProvider;\n    private readonly IOcelotLogger _logger;\n\n    public DelegatingHandlerFactory(\n        ITracingHandlerFactory tracingFactory,\n        IQoSFactory qoSFactory,\n        IServiceProvider serviceProvider,\n        IOcelotLoggerFactory loggerFactory)\n    {\n        _logger = loggerFactory.CreateLogger<DelegatingHandlerFactory>();\n        _serviceProvider = serviceProvider;\n        _tracingFactory = tracingFactory;\n        _qoSFactory = qoSFactory;\n    }\n\n    public List<DelegatingHandler> Get(DownstreamRoute route)\n    {\n        var globalDelegatingHandlers = _serviceProvider.GetServices<GlobalDelegatingHandler>()\n            .ToArray();\n        var routeSpecificHandlers = _serviceProvider.GetServices<DelegatingHandler>()\n            .ToList();\n        var handlers = new List<DelegatingHandler>();\n\n        foreach (var handler in globalDelegatingHandlers)\n        {\n            if (GlobalIsInHandlersConfig(route, handler))\n            {\n                routeSpecificHandlers.Add(handler.DelegatingHandler);\n            }\n            else\n            {\n                handlers.Add(handler.DelegatingHandler);\n            }\n        }\n\n        if (route.DelegatingHandlers.Count != 0)\n        {\n            var sorted = SortByConfigOrder(route, routeSpecificHandlers);\n            handlers.AddRange(sorted);\n        }\n\n        if (route.HttpHandlerOptions.UseTracing)\n        {\n            handlers.Add((DelegatingHandler)_tracingFactory.Get());\n        }\n\n        if (route.QosOptions.UseQos)\n        {\n            var handler = _qoSFactory.Get(route);\n            if (handler?.IsError == false)\n            {\n                handlers.Add(handler.Data);\n            }\n            else\n            {\n                _logger.LogWarning(() => $\"Route '{route.Name()}' specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!\");\n                handlers.Add(new NoQosDelegatingHandler());\n            }\n        }\n\n        return handlers;\n    }\n\n    private static DelegatingHandler[] SortByConfigOrder(DownstreamRoute request, List<DelegatingHandler> routeSpecificHandlers)\n    {\n        return routeSpecificHandlers\n            .Where(x => request.DelegatingHandlers.Contains(x.GetType().Name))\n            .OrderBy(d =>\n            {\n                var type = d.GetType().Name;\n                var pos = request.DelegatingHandlers.IndexOf(type);\n                return pos;\n            }).ToArray();\n    }\n\n    private static bool GlobalIsInHandlersConfig(DownstreamRoute request, GlobalDelegatingHandler handler) =>\n        request.DelegatingHandlers.Contains(handler.DelegatingHandler.GetType().Name);\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/GlobalDelegatingHandler.cs",
    "content": "namespace Ocelot.Requester;\n\npublic class GlobalDelegatingHandler\n{\n    public GlobalDelegatingHandler(DelegatingHandler delegatingHandler)\n    {\n        DelegatingHandler = delegatingHandler;\n    }\n\n    public DelegatingHandler DelegatingHandler { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/HttpExceptionToErrorMapper.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Errors;\nusing Ocelot.Request.Mapper;\n\nnamespace Ocelot.Requester;\n\npublic class HttpExceptionToErrorMapper : IExceptionToErrorMapper\n{\n    /// <summary>This is a dictionary of custom mappers for exceptions.</summary>\n    private readonly IDictionary<Type, Func<Exception, Error>> _mappers;\n\n    public HttpExceptionToErrorMapper(IServiceProvider serviceProvider)\n    {\n        _mappers = serviceProvider.GetService<IDictionary<Type, Func<Exception, Error>>>();\n    }\n\n    public Error Map(Exception exception)\n    {\n        var type = exception.GetType();\n\n        // If there is a custom mapper for this exception type, use it\n        // The idea is the following: When implementing features or providers,\n        // you can provide a custom mapper\n        if (_mappers != null && _mappers.TryGetValue(type, out var mapper))\n        {\n            return mapper(exception);\n        }\n\n        // here are mapped the exceptions thrown from Ocelot core application\n        if (type == typeof(TimeoutException))\n        {\n            return new RequestTimedOutError(exception);\n        }\n\n        if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException)))\n        {\n            return new RequestCanceledError(exception.Message);\n        }\n\n        if (type == typeof(HttpRequestException) || type == typeof(TimeoutException))\n        {\n            // Inner exception is a BadHttpRequestException, and only this exception exposes the StatusCode property.\n            // We check if the inner exception is a BadHttpRequestException and if the StatusCode is 413, we return a PayloadTooLargeError\n            if (exception.InnerException is BadHttpRequestException { StatusCode: StatusCodes.Status413RequestEntityTooLarge })\n            {\n                return new PayloadTooLargeError(exception);\n            }\n\n            return new ConnectionToDownstreamServiceError(exception);\n        }\n\n        return new UnableToCompleteRequestError(exception);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/IDelegatingHandlerFactory.cs",
    "content": "using Ocelot.Configuration;\n\nnamespace Ocelot.Requester;\n\npublic interface IDelegatingHandlerFactory\n{\n    List<DelegatingHandler> Get(DownstreamRoute route);\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/IExceptionToErrorMapper.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Requester;\n\npublic interface IExceptionToErrorMapper\n{\n    Error Map(Exception exception);\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/IHttpRequester.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Requester;\n\npublic interface IHttpRequester\n{\n    Task<Response<HttpResponseMessage>> GetResponse(HttpContext httpContext);\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/IMessageInvokerPool.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.Requester;\n\n/// <summary>\n/// A pool implementation for <see cref=\"HttpMessageInvoker\"/> pooling.\n/// <para>\n/// Largely inspired by StackExchange implementation.\n/// Link: <see href=\"https://github.com/StackExchange/StackExchange.Utils/blob/main/src/StackExchange.Utils.Http/DefaultHttpClientPool.cs\">StackExchange.Utils.DefaultHttpClientPool</see>.\n/// </para>\n/// </summary>\npublic interface IMessageInvokerPool\n{\n    /// <summary>\n    /// Gets a client for the specified <see cref=\"DownstreamRoute\"/>.\n    /// </summary>\n    /// <param name=\"downstreamRoute\">The route to get a Message Invoker for.</param>\n    /// <returns>A <see cref=\"HttpMessageInvoker\"/> from the pool.</returns>\n    HttpMessageInvoker Get(DownstreamRoute downstreamRoute);\n\n    /// <summary>\n    /// Clears the pool, in case you need to.\n    /// </summary>\n    void Clear();\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/MessageInvokerHttpRequester.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Requester;\n\npublic class MessageInvokerHttpRequester : IHttpRequester\n{\n    private readonly IOcelotLogger _logger;\n    private readonly IExceptionToErrorMapper _mapper;\n    private readonly IMessageInvokerPool _messageHandlerPool;\n\n    public MessageInvokerHttpRequester(IOcelotLoggerFactory loggerFactory,\n        IMessageInvokerPool messageHandlerPool,\n        IExceptionToErrorMapper mapper)\n    {\n        ArgumentNullException.ThrowIfNull(loggerFactory);\n        _logger = loggerFactory.CreateLogger<MessageInvokerHttpRequester>();\n\n        ArgumentNullException.ThrowIfNull(messageHandlerPool);\n        _messageHandlerPool = messageHandlerPool;\n\n        ArgumentNullException.ThrowIfNull(mapper);\n        _mapper = mapper;\n    }\n\n    public async Task<Response<HttpResponseMessage>> GetResponse(HttpContext httpContext)\n    {\n        var downstreamRequest = httpContext.Items.DownstreamRequest();\n        var messageInvoker = _messageHandlerPool.Get(httpContext.Items.DownstreamRoute());\n        try\n        {\n            var response = await messageInvoker.SendAsync(downstreamRequest.ToHttpRequestMessage(), httpContext.RequestAborted);\n            return new OkResponse<HttpResponseMessage>(response);\n        }\n        catch (Exception exception)\n        {\n            var error = _mapper.Map(exception);\n            return new ErrorResponse<HttpResponseMessage>(error);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/MessageInvokerPool.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing System.Net.Security;\n\nnamespace Ocelot.Requester;\n\npublic class MessageInvokerPool : IMessageInvokerPool\n{\n    private readonly ConcurrentDictionary<MessageInvokerCacheKey, Lazy<HttpMessageInvoker>> _handlersPool;\n    private readonly IDelegatingHandlerFactory _handlerFactory;\n    private readonly IOcelotLogger _logger;\n\n    public MessageInvokerPool(\n        IDelegatingHandlerFactory handlerFactory,\n        IOcelotLoggerFactory loggerFactory)\n    {\n        ArgumentNullException.ThrowIfNull(handlerFactory);\n        ArgumentNullException.ThrowIfNull(loggerFactory);\n\n        _handlersPool = new();\n        _handlerFactory = handlerFactory;\n        _logger = loggerFactory.CreateLogger<MessageInvokerPool>();\n    }\n\n    public virtual HttpMessageInvoker Get(DownstreamRoute downstreamRoute)\n    {\n        // Since the comparison is based on the downstream route object reference,\n        // and the QoS Options properties can't be changed after the route is created,\n        // we don't need to use the timeout value as part of the cache key.\n        return _handlersPool.GetOrAdd(\n            new MessageInvokerCacheKey(downstreamRoute),\n            cacheKey => new Lazy<HttpMessageInvoker>(() => CreateMessageInvoker(cacheKey.Route))\n        ).Value;\n    }\n\n    public virtual void Clear() => _handlersPool.Clear();\n\n    protected HttpMessageInvoker CreateMessageInvoker(DownstreamRoute route)\n    {\n        HttpMessageHandler baseHandler = CreateHandler(route);\n        List<DelegatingHandler> handlers = _handlerFactory.Get(route);\n        handlers.Reverse();\n        foreach (DelegatingHandler handler in handlers)\n        {\n            handler.InnerHandler = baseHandler;\n            baseHandler = handler;\n        }\n\n        int milliseconds = EnsureRouteTimeoutIsGreaterThanQosOne(route);\n        TimeSpan timeout = TimeSpan.FromMilliseconds(milliseconds);\n\n        // Adding timeout handler to the top of the chain.\n        // It's standard behavior to throw TimeoutException after the defined timeout (90 seconds by default)\n        HttpMessageHandler timeoutHandler = new TimeoutDelegatingHandler(timeout)\n        {\n            InnerHandler = baseHandler,\n        };\n        return new(timeoutHandler, true);\n    }\n\n    /// <summary>\n    /// Ensures that the route timeout is greater than the QoS timeout. If the route timeout is less than or equal to the QoS timeout, returns double the QoS timeout value and logs a warning.\n    /// </summary>\n    /// <remarks>The method is open for overriding because it is declared as <see langword=\"virtual\"/>.</remarks>\n    /// <param name=\"route\">Current processing route.</param>\n    /// <returns>An <see cref=\"int\"/> value representing the timeout in milliseconds, to be assigned in the upper context.</returns>\n    protected virtual int EnsureRouteTimeoutIsGreaterThanQosOne(DownstreamRoute route)\n    {\n        var qos = route.QosOptions;\n        int routeMilliseconds = 1_000 * (route.Timeout ?? DownstreamRoute.DefaultTimeoutSeconds);\n        if (!qos.UseQos || !qos.Timeout.HasValue || routeMilliseconds > qos.Timeout)\n        {\n            return routeMilliseconds;\n        }\n\n        int milliseconds = routeMilliseconds;\n        int doubledTimeout = 2 * qos.Timeout.Value;\n        Func<string> getWarning = route.Timeout.HasValue\n            ? () => $\"Route '{route.Name()}' has Quality of Service settings ({nameof(FileRoute.QoSOptions)}) enabled, but either the route {nameof(route.Timeout)} or the QoS {nameof(QoSOptions.Timeout)} is misconfigured: specifically, the route {nameof(route.Timeout)} ({milliseconds} ms) {EqualitySentence(milliseconds, qos.Timeout.Value)} the QoS {nameof(QoSOptions.Timeout)} ({qos.Timeout} ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS {nameof(QoSOptions.Timeout)} and applied {doubledTimeout} ms to the route {nameof(route.Timeout)}. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\"\n            : () => $\"Route '{route.Name()}' has Quality of Service settings ({nameof(FileRoute.QoSOptions)}) enabled, but either the {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)} or the QoS {nameof(QoSOptions.Timeout)} is misconfigured: specifically, the {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)} ({milliseconds} ms) {EqualitySentence(milliseconds, qos.Timeout.Value)} the QoS {nameof(QoSOptions.Timeout)} ({qos.Timeout} ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS {nameof(QoSOptions.Timeout)} and applied {doubledTimeout} ms to the route {nameof(route.Timeout)} instead of using {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)}. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\";\n        _logger.LogWarning(getWarning);\n        return doubledTimeout;\n    }\n\n    public static string EqualitySentence(int left, int right)\n        => left < right ? \"is shorter than\" : left == right ? \"is equal to\" : \"is longer than\";\n\n    protected virtual SocketsHttpHandler CreateHandler(DownstreamRoute route)\n    {\n        var options = route.HttpHandlerOptions;\n        var handler = new SocketsHttpHandler\n        {\n            AllowAutoRedirect = options.AllowAutoRedirect,\n            UseCookies = options.UseCookieContainer,\n            UseProxy = options.UseProxy,\n            MaxConnectionsPerServer = options.MaxConnectionsPerServer,\n            PooledConnectionLifetime = options.PooledConnectionLifeTime,\n        };\n\n        if (options.UseCookieContainer)\n        {\n            handler.CookieContainer = new CookieContainer();\n        }\n\n        if (!route.DangerousAcceptAnyServerCertificateValidator)\n        {\n            return handler;\n        }\n\n        handler.SslOptions = new SslClientAuthenticationOptions\n        {\n            RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,\n        };\n        _logger.LogWarning(() =>\n            $\"You have ignored all SSL warnings by using {nameof(DownstreamRoute.DangerousAcceptAnyServerCertificateValidator)} for this {nameof(DownstreamRoute)} -> {route.Name()}\");\n        return handler;\n    }\n\n    public readonly struct MessageInvokerCacheKey : IEquatable<MessageInvokerCacheKey>\n    {\n        public MessageInvokerCacheKey(DownstreamRoute route) => Route = route;\n\n        public DownstreamRoute Route { get; }\n\n        public override bool Equals(object obj) => obj is MessageInvokerCacheKey key && Equals(key);\n\n        public bool Equals(MessageInvokerCacheKey other) =>\n            EqualityComparer<DownstreamRoute>.Default.Equals(Route, other.Route);\n\n        public override int GetHashCode() => Route.GetHashCode();\n\n        public static bool operator ==(MessageInvokerCacheKey left, MessageInvokerCacheKey right) => left.Equals(right);\n        public static bool operator !=(MessageInvokerCacheKey left, MessageInvokerCacheKey right) => !(left == right);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Requester.Middleware;\n\npublic class HttpRequesterMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IHttpRequester _requester;\n\n    public HttpRequesterMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IHttpRequester requester)\n            : base(loggerFactory.CreateLogger<HttpRequesterMiddleware>())\n    {\n        _next = next;\n        _requester = requester;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var response = await _requester.GetResponse(httpContext);\n        CreateLogBasedOnResponse(response);\n\n        if (response.IsError)\n        {\n            Logger.LogDebug(\"IHttpRequester returned an error, setting pipeline error\");\n\n            httpContext.Items.UpsertErrors(response.Errors);\n            return;\n        }\n\n        Logger.LogDebug(\"Setting HTTP response message...\");\n        httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(response.Data));\n        await _next.Invoke(httpContext);\n    }\n\n    private void CreateLogBasedOnResponse(Response<HttpResponseMessage> response)\n    {\n        var status = response.Data?.StatusCode ?? HttpStatusCode.Processing;\n        var reason = response.Data?.ReasonPhrase ?? \"unknown\";\n        var uri = response.Data?.RequestMessage?.RequestUri?.ToString() ?? string.Empty;\n\n        string message() => $\"{(int)status} {reason} status code of request URI: {uri}\";\n\n        if (status < HttpStatusCode.BadRequest)\n        {\n            Logger.LogInformation(message);\n        }\n        else\n        {\n            Logger.LogWarning(message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/RequestCanceledError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Requester;\n\npublic class RequestCanceledError : Error\n{\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"RequestCanceledError\"/> class.\n    /// Creates <see cref=\"RequestCanceledError\"/> object by the message.\n    /// <para>Status code refer to:</para>\n    /// <para>https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top .</para>\n    /// <para>https://httpstatuses.com/499 .</para>\n    /// </summary>\n    /// <param name=\"message\">The message text.</param>\n    public RequestCanceledError(string message)\n        : base(message, OcelotErrorCode.RequestCanceled, 499) // https://httpstatuses.com/499\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/ServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\n\nnamespace Ocelot.Requester;\n\npublic static class ServiceCollectionExtensions\n{\n    public static void AddOcelotMessageInvokerPool(this IServiceCollection services)\n    {\n        services.AddSingleton<IHttpRequester, MessageInvokerHttpRequester>();\n        services.AddSingleton<IMessageInvokerPool, MessageInvokerPool>();\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/TimeoutDelegatingHandler.cs",
    "content": "﻿namespace Ocelot.Requester;\n\n/// <summary>\n/// TODO: Next subjects of investigation are:\n/// <list type=\"bullet\">\n/// <item><see href=\"https://learn.microsoft.com/en-us/aspnet/core/performance/timeouts\">Request timeouts middleware in ASP.NET Core</see></item>\n/// <item><see href=\"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-9.0#behavior-with-debugger-attached\">Kestrel timeouts</see></item>\n/// </list>\n/// </summary>\npublic class TimeoutDelegatingHandler : DelegatingHandler\n{\n    private readonly TimeSpan _timeout;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"TimeoutDelegatingHandler\"/> class.\n    /// </summary>\n    /// <param name=\"timeout\">The time span after which the request is cancelled.</param>\n    public TimeoutDelegatingHandler(TimeSpan timeout)\n    {\n        _timeout = timeout;\n    }\n\n    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        cts.CancelAfter(_timeout);\n\n        try\n        {\n            return await base.SendAsync(request, cts.Token);\n        }\n        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)\n        {\n            throw new TimeoutException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Requester/UnableToCompleteRequestError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.Requester;\n\npublic class UnableToCompleteRequestError : Error\n{\n    public UnableToCompleteRequestError(Exception exception)\n        : base($\"Error making http request, exception: {exception}\", OcelotErrorCode.UnableToCompleteRequestError, 500)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\n\nnamespace Ocelot.Responder;\n\npublic class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper\n{\n    public int Map(List<Error> errors)\n    {\n        if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError))\n        {\n            return 401;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError\n            || e.Code == OcelotErrorCode.ClaimValueNotAuthorizedError\n            || e.Code == OcelotErrorCode.ScopeNotAuthorizedError\n            || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError\n            || e.Code == OcelotErrorCode.CannotFindClaimError))\n        {\n            return 403;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.QuotaExceededError))\n        {\n            return errors.Single(e => e.Code == OcelotErrorCode.QuotaExceededError).HttpStatusCode;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError))\n        {\n            return StatusCodes.Status503ServiceUnavailable;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.RequestCanceled))\n        {\n            // status code refer to\n            // https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top\n            // https://httpstatuses.com/499\n            return StatusCodes.Status499ClientClosedRequest;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError))\n        {\n            return 404;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.ConnectionToDownstreamServiceError))\n        {\n            return 502;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError\n            || e.Code == OcelotErrorCode.CouldNotFindLoadBalancerCreator\n            || e.Code == OcelotErrorCode.ErrorInvokingLoadBalancerCreator))\n        {\n            return 500;\n        }\n\n        if (errors.Any(e => e.Code == OcelotErrorCode.PayloadTooLargeError))\n        {\n            return 413;\n        }\n\n        return 404;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Responder/HttpContextResponder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Headers;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Responder;\n\n/// <summary>\n/// Cannot unit test things in this class due to methods not being implemented on .NET concretes used for testing.\n/// </summary>\npublic class HttpContextResponder : IHttpResponder\n{\n    private readonly IRemoveOutputHeaders _removeOutputHeaders;\n\n    public HttpContextResponder(IRemoveOutputHeaders removeOutputHeaders)\n    {\n        _removeOutputHeaders = removeOutputHeaders;\n    }\n\n    public async Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse downstream)\n    {\n        _removeOutputHeaders.Remove(downstream.Headers);\n\n        foreach (var httpResponseHeader in downstream.Headers)\n        {\n            AddHeaderIfDoesntExist(context, httpResponseHeader);\n        }\n\n        SetStatusCode(context, (int)downstream.StatusCode);\n\n        context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = downstream.ReasonPhrase;\n\n        // As of 5.0 HttpResponse.Content never returns null.\n        // https://github.com/dotnet/runtime/blame/8fc68f626a11d646109a758cb0fc70a0aa7826f1/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L46\n        // TODO: Check if it applies to ocelot custom implementation\n        if (downstream.Content is null)\n        {\n            return;\n        }\n\n        foreach (var httpResponseHeader in downstream.Content.Headers)\n        {\n            AddHeaderIfDoesntExist(context, new Header(httpResponseHeader.Key, httpResponseHeader.Value));\n        }\n\n        if (downstream.Content.Headers.ContentLength != null)\n        {\n            AddHeaderIfDoesntExist(context,\n                new Header(\"Content-Length\", new[] { downstream.Content.Headers.ContentLength.ToString() }));\n        }\n\n        if (downstream.StatusCode != HttpStatusCode.NotModified && context.Response.ContentLength != 0)\n        {\n            await WriteToUpstreamAsync(context, downstream);\n        }\n    }\n\n    public void SetErrorResponseOnContext(HttpContext context, int statusCode)\n    {\n        SetStatusCode(context, statusCode);\n    }\n\n    public async Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse downstream)\n    {\n        if (downstream.Content.Headers.ContentLength != null)\n        {\n            AddHeaderIfDoesntExist(context,\n                new Header(\"Content-Length\", new[] { downstream.Content.Headers.ContentLength.ToString() }));\n        }\n\n        if (context.Response.ContentLength != 0)\n        {\n            await WriteToUpstreamAsync(context, downstream);\n        }\n    }\n\n    protected virtual async Task WriteToUpstreamAsync(HttpContext context, DownstreamResponse downstream)\n    {\n        await using var content = await downstream.Content.ReadAsStreamAsync();\n        await content.CopyToAsync(context.Response.Body, context.RequestAborted);\n    }\n\n    private static void SetStatusCode(HttpContext context, int statusCode)\n    {\n        if (!context.Response.HasStarted)\n        {\n            context.Response.StatusCode = statusCode;\n        }\n    }\n\n    private static void AddHeaderIfDoesntExist(HttpContext context, Header httpResponseHeader)\n    {\n        if (!context.Response.Headers.ContainsKey(httpResponseHeader.Key))\n        {\n            context.Response.Headers.Append(\n                httpResponseHeader.Key,\n                new StringValues(httpResponseHeader.Values.ToArray()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs",
    "content": "﻿using Ocelot.Errors;\n\r\nnamespace Ocelot.Responder;\n\n/// <summary>\r\n/// Defines mapping a list of Ocelot errors to a single appropriate HTTP status code.\r\n/// </summary>\r\npublic interface IErrorsToHttpStatusCodeMapper\n{\n    /// <summary>\n    /// Maps a list of Ocelot <see cref=\"Error\"/> to a single appropriate HTTP status code.\n    /// </summary>\n    /// <param name=\"errors\">The collection of errors.</param>\n    /// <returns>An integer value with HTTP status code.</returns>\n    int Map(List<Error> errors);\n}\n"
  },
  {
    "path": "src/Ocelot/Responder/IHttpResponder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Middleware;\n\r\nnamespace Ocelot.Responder;\r\n\npublic interface IHttpResponder\r\n{\r\n    Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response);\r\n\r\n    void SetErrorResponseOnContext(HttpContext context, int statusCode);\n\n    Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Responder/Middleware/ResponderMiddleware.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Responder.Middleware;\n\n/// <summary>\n/// Completes and returns the request and request body, if any pipeline errors occured then sets the appropriate HTTP status code instead.\n/// </summary>\npublic class ResponderMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IHttpResponder _responder;\n    private readonly IErrorsToHttpStatusCodeMapper _codeMapper;\n\n    public ResponderMiddleware(RequestDelegate next,\n        IHttpResponder responder,\n        IOcelotLoggerFactory loggerFactory,\n        IErrorsToHttpStatusCodeMapper codeMapper)\n        : base(loggerFactory.CreateLogger<ResponderMiddleware>())\n    {\n        _next = next;\n        _responder = responder;\n        _codeMapper = codeMapper;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        await _next.Invoke(context);\n\n        var errors = context.Items.Errors();\n        if (errors.Count > 0)\n        {\n            Logger.LogWarning(() => $\"{MiddlewareName} found {errors.Count} error{errors.Count.Plural()} ->{errors.ToErrorString(true, true)}Setting error response for request: {context.Request.Method} {context.Request.Path}\");\n            await SetErrorResponse(context, errors);\n            return;\n        }\n\n        // We are going to dispose the http request message and content in\n        // this middleware (no further use). That's why we are using the 'using' statement.\n        using var response = context.Items.DownstreamResponse();\n        if (response == null)\n        {\n            Logger.LogDebug(() => $\"Pipeline was terminated early in {MiddlewareName}\");\n            return;\n        }\n\n        Logger.LogDebug(\"No pipeline errors: setting and returning completed response...\");\n        await _responder.SetResponseOnHttpContext(context, response);\n    }\n\n    private async Task SetErrorResponse(HttpContext context, List<Error> errors)\n    {\n        // TODO The exception/error handling should be reviewed and refactored.\n        var statusCode = _codeMapper.Map(errors);\n        _responder.SetErrorResponseOnContext(context, statusCode);\n\n        if (errors.All(e => e.Code != OcelotErrorCode.QuotaExceededError))\n        {\n            return;\n        }\n\n        var downstreamResponse = context.Items.DownstreamResponse();\n        await _responder.SetErrorResponseOnContext(context, downstreamResponse);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Responses/ErrorResponse.cs",
    "content": "using Ocelot.Errors;\n\r\nnamespace Ocelot.Responses;\r\n\r\npublic class ErrorResponse : Response\r\n{\r\n    public ErrorResponse(Error error)\n        : base(new() { error })\r\n    { }\r\n\r\n    public ErrorResponse(List<Error> errors)\n        : base(errors)\r\n    { }\r\n}\r\n\r\npublic class ErrorResponse<T> : Response<T>\r\n{\r\n    public ErrorResponse(Error error)\n        : base(new List<Error> { error })\r\n    { }\r\n\r\n    public ErrorResponse(List<Error> errors)\n        : base(errors)\r\n    { }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Responses/OkResponse.cs",
    "content": "namespace Ocelot.Responses;\r\n\r\npublic class OkResponse : Response\r\n{\r\n    public OkResponse() { }\r\n}\r\n\r\npublic class OkResponse<T> : Response<T>\r\n{\r\n    public OkResponse(T data) : base(data) { }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Responses/Response.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.Responses;\n\npublic abstract class Response\n{\n    protected Response()\n    {\n        Errors = new List<Error>();\n    }\n\n    protected Response(List<Error> errors)\n    {\n        Errors = errors ?? new List<Error>();\n    }\n\n    public List<Error> Errors { get; }\n\n    public bool IsError => Errors.Count > 0;\n}\n\npublic abstract class Response<T> : Response\n{\n    protected Response(T data)\n    {\n        Data = data;\n    }\n\n    protected Response(List<Error> errors) : base(errors)\n    { }\n\n    public T Data { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Security.IPSecurity;\n\npublic class IPSecurityPolicy : ISecurityPolicy\n{\n    public Response Security(DownstreamRoute downstreamRoute, HttpContext context)\n    {\n        var clientIp = context.Connection.RemoteIpAddress;\n        var options = downstreamRoute.SecurityOptions;\n        if (options == null || clientIp == null)\n        {\n            return new OkResponse();\n        }\n\n        if (options.IPBlockedList?.Count > 0)\n        {\n            if (options.IPBlockedList.Contains(clientIp.ToString()))\n            {\n                var error = new UnauthenticatedError($\"This request rejects access to {clientIp} IP\");\n                return new ErrorResponse(error);\n            }\n        }\n\n        if (options.IPAllowedList?.Count > 0)\n        {\n            if (!options.IPAllowedList.Contains(clientIp.ToString()))\n            {\n                var error = new UnauthenticatedError($\"{clientIp} does not allow access, the request is invalid\");\n                return new ErrorResponse(error);\n            }\n        }\n\n        return new OkResponse();\n    }\n\n    public Task<Response> SecurityAsync(DownstreamRoute downstreamRoute, HttpContext context)\n        => Task.Run(() => Security(downstreamRoute, context));\n}\n"
  },
  {
    "path": "src/Ocelot/Security/ISecurityPolicy.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Security;\n\npublic interface ISecurityPolicy\n{\n    Response Security(DownstreamRoute downstreamRoute, HttpContext context);\n    Task<Response> SecurityAsync(DownstreamRoute downstreamRoute, HttpContext context);\n}\n"
  },
  {
    "path": "src/Ocelot/Security/Middleware/SecurityMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Security.Middleware;\n\npublic class SecurityMiddleware : OcelotMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly IEnumerable<ISecurityPolicy> _securityPolicies;\n\n    public SecurityMiddleware(RequestDelegate next,\n        IOcelotLoggerFactory loggerFactory,\n        IEnumerable<ISecurityPolicy> securityPolicies)\n        : base(loggerFactory.CreateLogger<SecurityMiddleware>())\n    {\n        _securityPolicies = securityPolicies;\n        _next = next;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var downstreamRoute = httpContext.Items.DownstreamRoute();\n\n        if (_securityPolicies != null)\n        {\n            foreach (var policy in _securityPolicies)\n            {\n                var result = policy.Security(downstreamRoute, httpContext);\n                if (!result.IsError)\n                {\n                    continue;\n                }\n\n                httpContext.Items.UpsertErrors(result.Errors);\n                return;\n            }\n        }\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/Configuration/ServiceFabricConfiguration.cs",
    "content": "﻿namespace Ocelot.ServiceDiscovery.Configuration;\n\npublic class ServiceFabricConfiguration\n{\n    public ServiceFabricConfiguration(string hostName, int port, string serviceName)\n    {\n        HostName = hostName;\n        Port = port;\n        ServiceName = serviceName;\n    }\n\n    public string ServiceName { get; }\n\n    public string HostName { get; }\n\n    public int Port { get; }\n}\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Responses;\r\nusing Ocelot.ServiceDiscovery.Providers;\n\r\nnamespace Ocelot.ServiceDiscovery;\r\n\r\npublic interface IServiceDiscoveryProviderFactory\r\n{\r\n    Response<IServiceDiscoveryProvider> Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs",
    "content": "using Ocelot.Values;\n\nnamespace Ocelot.ServiceDiscovery.Providers;\n\npublic class ConfigurationServiceProvider : IServiceDiscoveryProvider\n{\n    private readonly List<Service> _services;\n\n    public ConfigurationServiceProvider(List<Service> services) => _services = services;\n\n    public Task<List<Service>> GetAsync() => ValueTask.FromResult(_services).AsTask();\n}\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs",
    "content": "using Ocelot.Values;\n\nnamespace Ocelot.ServiceDiscovery.Providers;\n\npublic interface IServiceDiscoveryProvider\n{\n    Task<List<Service>> GetAsync();\n}\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs",
    "content": "﻿using Ocelot.ServiceDiscovery.Configuration;\nusing Ocelot.Values;\n\nnamespace Ocelot.ServiceDiscovery.Providers;\n\npublic class ServiceFabricServiceDiscoveryProvider : IServiceDiscoveryProvider\n{\n    public const string Type = \"ServiceFabric\"; // TODO This property should be defined in the IServiceDiscoveryProvider interface\n\n    private readonly ServiceFabricConfiguration _configuration;\n\n    public ServiceFabricServiceDiscoveryProvider(ServiceFabricConfiguration configuration)\n    {\n        _configuration = configuration;\n    }\n\n    public Task<List<Service>> GetAsync()\n    {\n        return Task.FromResult(new List<Service>\n        {\n            new(_configuration.ServiceName,\n                new ServiceHostAndPort(_configuration.HostName, _configuration.Port),\n                \"doesnt matter with service fabric\",\n                \"doesnt matter with service fabric\",\n                new List<string>()),\n        });\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.ServiceDiscovery.Providers;\n\r\nnamespace Ocelot.ServiceDiscovery;\r\n\r\npublic delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route);\r\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Responses;\r\nusing Ocelot.ServiceDiscovery.Configuration;\r\nusing Ocelot.ServiceDiscovery.Providers;\r\nusing Ocelot.Values;\r\n\r\nnamespace Ocelot.ServiceDiscovery;\r\n\r\npublic class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory\r\n{\r\n    private readonly IServiceProvider _provider;\r\n    private readonly ServiceDiscoveryFinderDelegate _delegates;\r\n    private readonly IOcelotLogger _logger;\r\n\r\n    public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider)\r\n    {\r\n        _provider = provider;\r\n        _delegates = provider.GetService<ServiceDiscoveryFinderDelegate>();\r\n        _logger = factory.CreateLogger<ServiceDiscoveryProviderFactory>();\r\n    }\r\n\r\n    public Response<IServiceDiscoveryProvider> Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route)\r\n    {\r\n        if (route.UseServiceDiscovery)\r\n        {\r\n            _logger.LogInformation(() => $\"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{route.Name()}' is enabled.\");\r\n            return GetServiceDiscoveryProvider(serviceConfig, route);\r\n        }\r\n\r\n        var services = route.DownstreamAddresses\r\n            .Select(address => new Service(\r\n                route.ServiceName,\r\n                new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme),\r\n                string.Empty,\r\n                string.Empty,\r\n                Enumerable.Empty<string>()))\r\n            .ToList();\r\n\r\n        return new OkResponse<IServiceDiscoveryProvider>(new ConfigurationServiceProvider(services));\r\n    }\r\n\r\n    private Response<IServiceDiscoveryProvider> GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route)\r\n    {\r\n        _logger.LogInformation(() => $\"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'...\");\r\n\r\n        if (ServiceFabricServiceDiscoveryProvider.Type.Equals(config.Type, StringComparison.OrdinalIgnoreCase))\r\n        {\r\n            var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName);\r\n            return new OkResponse<IServiceDiscoveryProvider>(new ServiceFabricServiceDiscoveryProvider(sfConfig));\r\n        }\r\n\r\n        if (_delegates != null)\r\n        {\r\n            var provider = _delegates?.Invoke(_provider, config, route);\r\n            if (provider.GetType().Name.Equals(config.Type, StringComparison.OrdinalIgnoreCase))\r\n            {\r\n                return new OkResponse<IServiceDiscoveryProvider>(provider);\r\n            }\r\n        }\r\n\r\n        var message = $\"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!\";\r\n        _logger.LogWarning(() => $\"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!\");\r\n\r\n        return new ErrorResponse<IServiceDiscoveryProvider>(new UnableToFindServiceDiscoveryProviderError(message));\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs",
    "content": "using Ocelot.Errors;\n\nnamespace Ocelot.ServiceDiscovery;\n\npublic class UnableToFindServiceDiscoveryProviderError : Error\n{\n    public UnableToFindServiceDiscoveryProviderError(string message) : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using System.Collections.Concurrent;\nglobal using System.Net;\nglobal using System.Text;\nglobal using System.Text.RegularExpressions;\n"
  },
  {
    "path": "src/Ocelot/Values/DownstreamPath.cs",
    "content": "﻿namespace Ocelot.Values;\r\n\r\npublic class DownstreamPath\r\n{\r\n    public DownstreamPath(string value)\r\n    {\r\n        Value = value;\r\n    }\r\n\r\n    public string Value { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Values/DownstreamPathTemplate.cs",
    "content": "﻿namespace Ocelot.Values;\n\npublic class DownstreamPathTemplate\n{\n    public DownstreamPathTemplate(string value)\n    {\n        Value = value;\n    }\n\n    public string Value { get; }\n\n    public override string ToString() => Value ?? string.Empty;\n}\n"
  },
  {
    "path": "src/Ocelot/Values/Service.cs",
    "content": "namespace Ocelot.Values;\r\n\r\npublic class Service\r\n{\r\n    public Service(string name,\r\n        ServiceHostAndPort hostAndPort,\r\n        string id,\r\n        string version,\r\n        IEnumerable<string> tags)\r\n    {\r\n        Name = name;\r\n        HostAndPort = hostAndPort;\r\n        Id = id;\r\n        Version = version;\r\n        Tags = tags;\r\n    }\r\n\r\n    public string Id { get; }\r\n\r\n    public string Name { get; }\r\n\r\n    public string Version { get; }\r\n\r\n    public IEnumerable<string> Tags { get; }\r\n\r\n    public ServiceHostAndPort HostAndPort { get; }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Values/ServiceHostAndPort.cs",
    "content": "﻿namespace Ocelot.Values;\r\n\r\npublic class ServiceHostAndPort : IEquatable<ServiceHostAndPort>\r\n{\r\n    public ServiceHostAndPort(ServiceHostAndPort from)\r\n    {\r\n        DownstreamHost = from.DownstreamHost;\r\n        DownstreamPort = from.DownstreamPort;\r\n        Scheme = from.Scheme;\r\n    }\r\n\r\n    public ServiceHostAndPort(string downstreamHost, int downstreamPort)\r\n    {\r\n        DownstreamHost = downstreamHost?.Trim('/');\r\n        DownstreamPort = downstreamPort;\r\n    }\r\n\r\n    public ServiceHostAndPort(string downstreamHost, int downstreamPort, string scheme)\r\n        : this(downstreamHost, downstreamPort) => Scheme = scheme;\r\n\r\n    public string DownstreamHost { get; }\r\n    public int DownstreamPort { get; }\n    public string Scheme { get; }\r\n\r\n    public override string ToString()\r\n        => $\"{Scheme}:{DownstreamHost}:{DownstreamPort}\";\r\n    public override int GetHashCode()\r\n        => Tuple.Create(Scheme, DownstreamHost, DownstreamPort).GetHashCode();\r\n\r\n    public bool Equals(ServiceHostAndPort other) => this == other;\r\n    public override bool Equals(object obj)\r\n        => obj != null && obj is ServiceHostAndPort o && this == o;\r\n\r\n    /// <summary>Checks equality of two hosts.</summary>\r\n    /// <remarks>Microsoft Learn | .NET | C# Docs:\r\n    ///   <list type=\"bullet\">\r\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/equality-operators\">Equality operators</seealso></item>\r\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-equals\">System.Object.Equals method</seealso></item>\r\n    ///   <item><seealso href=\"https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1.equals?view=net-8.0\">IEquatable&lt;T&gt;.Equals(T) Method</seealso></item>\r\n    ///   </list>\r\n    /// </remarks>\r\n    /// <param name=\"l\">Left operand.</param>\r\n    /// <param name=\"r\">Right operand.</param>\r\n    /// <returns><see langword=\"true\"/> if both operands are equal; otherwise, <see langword=\"false\"/>.</returns>\r\n    public static bool operator ==(ServiceHostAndPort l, ServiceHostAndPort r)\r\n        => (((object)l) == null || ((object)r) == null)\r\n            ? Equals(l, r)\r\n            : l.DownstreamHost == r.DownstreamHost && l.DownstreamPort == r.DownstreamPort && l.Scheme == r.Scheme;\r\n\r\n    public static bool operator !=(ServiceHostAndPort l, ServiceHostAndPort r)\r\n        => (((object)l) == null || ((object)r) == null)\r\n            ? !Equals(l, r)\r\n            : !(l == r);\r\n}\r\n"
  },
  {
    "path": "src/Ocelot/Values/UpstreamHeaderTemplate.cs",
    "content": "﻿using Ocelot.Infrastructure;\n\nnamespace Ocelot.Values;\n\n/// <summary>\n/// Upstream template properties of headers and their regular expression.\n/// </summary>\n/// <remarks>Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers\">Routing based on request header</see>.</remarks>\npublic class UpstreamHeaderTemplate\n{\n    public string Template { get; }\n    public string OriginalValue { get; }\n    public Regex Pattern { get; }\n\n    public UpstreamHeaderTemplate(string template, string originalValue)\n    {\n        Template = template;\n        OriginalValue = originalValue;\n        Pattern = RegexGlobal.New(template ?? \"$^\", RegexOptions.Singleline);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/Values/UpstreamPathTemplate.cs",
    "content": "using Ocelot.Infrastructure;\n\nnamespace Ocelot.Values;\n\n/// <summary>The model to keep data of upstream path.</summary>\npublic partial class UpstreamPathTemplate\n{\n    [GeneratedRegex(\"$^\", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\n    private static partial Regex RegexNoTemplate();\n\n    public UpstreamPathTemplate(string template, int priority, bool containsQueryString, string originalValue)\n    {\n        Template = template;\n        Priority = priority;\n        ContainsQueryString = containsQueryString;\n        OriginalValue = originalValue;\n    }\n\n    public string Template { get; }\n    public int Priority { get; }\n    public bool ContainsQueryString { get; }\n    public string OriginalValue { get; }\n\n    private Regex _pattern;\n    public Regex Pattern\n    {\n        get => _pattern;\n        set => _pattern = Template == null || value == null ? RegexNoTemplate() : value;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/ClientWebSocketConnector.cs",
    "content": "﻿using System.Net.WebSockets;\n\nnamespace Ocelot.WebSockets;\n\npublic class ClientWebSocketConnector : IClientWebSocketConnector\n{\n    private readonly ClientWebSocket _webSocket;\n    private readonly IClientWebSocketOptions _options;\n\n    public ClientWebSocketConnector(ClientWebSocket webSocket)\n    {\n        _webSocket = webSocket;\n        _options = new ClientWebSocketOptionsProxy(webSocket.Options);\n    }\n\n    public WebSocket ToWebSocket() => _webSocket;\n\n    public IClientWebSocketOptions Options => _options;\n\n    public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)\n        => _webSocket.ConnectAsync(uri, cancellationToken);\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/ClientWebSocketOptionsProxy.cs",
    "content": "﻿using System.Net.Security;\nusing System.Net.WebSockets;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.WebSockets;\n\npublic class ClientWebSocketOptionsProxy : IClientWebSocketOptions\n{\n    private readonly ClientWebSocketOptions _real;\n\n    public ClientWebSocketOptionsProxy(ClientWebSocketOptions options)\n    {\n        _real = options;\n    }\n\n    // TODO The design should be reviewed since we are hiding the ClientWebSocketOptions properties.\n    public Version HttpVersion { get => _real.HttpVersion; set => _real.HttpVersion = value; }\n    public HttpVersionPolicy HttpVersionPolicy { get => _real.HttpVersionPolicy; set => _real.HttpVersionPolicy = value; }\n    public bool UseDefaultCredentials { get => _real.UseDefaultCredentials; set => _real.UseDefaultCredentials = value; }\n    public ICredentials Credentials { get => _real.Credentials; set => _real.Credentials = value; }\n    public IWebProxy Proxy { get => _real.Proxy; set => _real.Proxy = value; }\n    public X509CertificateCollection ClientCertificates { get => _real.ClientCertificates; set => _real.ClientCertificates = value; }\n    public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get => _real.RemoteCertificateValidationCallback; set => _real.RemoteCertificateValidationCallback = value; }\n    public CookieContainer Cookies { get => _real.Cookies; set => _real.Cookies = value; }\n    public TimeSpan KeepAliveInterval { get => _real.KeepAliveInterval; set => _real.KeepAliveInterval = value; }\n    public WebSocketDeflateOptions DangerousDeflateOptions { get => _real.DangerousDeflateOptions; set => _real.DangerousDeflateOptions = value; }\n    public bool CollectHttpResponseDetails { get => _real.CollectHttpResponseDetails; set => _real.CollectHttpResponseDetails = value; }\n\n    public void AddSubProtocol(string subProtocol) => _real.AddSubProtocol(subProtocol);\n    public void SetBuffer(int receiveBufferSize, int sendBufferSize) => _real.SetBuffer(receiveBufferSize, sendBufferSize);\n    public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<byte> buffer) => _real.SetBuffer(receiveBufferSize, sendBufferSize, buffer);\n    public void SetRequestHeader(string headerName, string headerValue) => _real.SetRequestHeader(headerName, headerValue);\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/ClientWebSocketProxy.cs",
    "content": "﻿using System.Net.WebSockets;\n\nnamespace Ocelot.WebSockets;\n\npublic sealed class ClientWebSocketProxy : WebSocket, IClientWebSocket\n{\n    // RealSubject (Service) class of Proxy design pattern\n    private readonly WebSocket _realSocket;\n    private readonly IClientWebSocketConnector _connector;\n\n    public ClientWebSocketProxy(WebSocket socket, IClientWebSocketConnector connector)\n    {\n        _realSocket = socket;\n        _connector = connector;\n    }\n\n    // IClientWebSocketConnector implementations\n    public WebSocket ToWebSocket() => _realSocket;\n    public IClientWebSocketOptions Options => _connector.Options;\n    public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)\n        => _connector.ConnectAsync(uri, cancellationToken);\n\n    // WebSocket implementations\n    public override WebSocketCloseStatus? CloseStatus => _realSocket.CloseStatus;\n\n    public override string CloseStatusDescription => _realSocket.CloseStatusDescription;\n\n    public override WebSocketState State => _realSocket.State;\n\n    public override string SubProtocol => _realSocket.SubProtocol;\n\n    public override void Abort() => _realSocket.Abort();\n\n    public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)\n        => _realSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);\n\n    public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)\n        => _realSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken);\n\n    public override void Dispose() => _realSocket.Dispose();\n\n    public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)\n        => _realSocket.ReceiveAsync(buffer, cancellationToken);\n\n    public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)\n        => _realSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken);\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/IClientWebSocket.cs",
    "content": "﻿using System.Net.WebSockets;\n\nnamespace Ocelot.WebSockets;\n\npublic interface IClientWebSocket : IClientWebSocketConnector\n{\n    // WebSocket definitions\n    WebSocketCloseStatus? CloseStatus { get; }\n    string CloseStatusDescription { get; }\n    WebSocketState State { get; }\n    string SubProtocol { get; }\n    void Abort();\n    Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken);\n    Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken);\n    void Dispose();\n    Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken);\n    Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);\n}\n\npublic interface IClientWebSocketConnector\n{\n    WebSocket ToWebSocket();\n    IClientWebSocketOptions Options { get; }\n    Task ConnectAsync(Uri uri, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/IClientWebSocketOptions.cs",
    "content": "﻿using System.Net.Security;\nusing System.Net.WebSockets;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.WebSockets;\n\npublic interface IClientWebSocketOptions\n{\n    Version HttpVersion { get; set; }\n    HttpVersionPolicy HttpVersionPolicy { get; set; }\n    void SetRequestHeader(string headerName, string headerValue);\n    bool UseDefaultCredentials { get; set; }\n    ICredentials Credentials { get; set; }\n    IWebProxy Proxy { get; set; }\n    X509CertificateCollection ClientCertificates { get; set; }\n    RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }\n    CookieContainer Cookies { get; set; }\n    void AddSubProtocol(string subProtocol);\n    TimeSpan KeepAliveInterval { get; set; }\n    WebSocketDeflateOptions DangerousDeflateOptions { get; set; }\n    void SetBuffer(int receiveBufferSize, int sendBufferSize);\n    void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<byte> buffer);\n    bool CollectHttpResponseDetails { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/IWebSocketsFactory.cs",
    "content": "﻿namespace Ocelot.WebSockets;\n\npublic interface IWebSocketsFactory\n{\n    IClientWebSocket CreateClient();\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/WebSocketsFactory.cs",
    "content": "﻿using System.Net.WebSockets;\n\nnamespace Ocelot.WebSockets;\n\npublic class WebSocketsFactory : IWebSocketsFactory\n{\n    public IClientWebSocket CreateClient()\n    {\n        var socket = new ClientWebSocket();\n        var connector = new ClientWebSocketConnector(socket);\n        return new ClientWebSocketProxy(socket, connector);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs",
    "content": "﻿// Copyright (c) .NET Foundation. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.\n// Modified https://github.com/aspnet/Proxy websockets class to use in Ocelot.\n\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing System.Net.WebSockets;\n\nnamespace Ocelot.WebSockets;\n\npublic class WebSocketsProxyMiddleware : OcelotMiddleware\n{\n    public static readonly string[] NotForwardedWebSocketHeaders = new[]\n    {\n        \"Connection\", \"Host\", \"Upgrade\",\n        \"Sec-WebSocket-Accept\", \"Sec-WebSocket-Protocol\", \"Sec-WebSocket-Key\", \"Sec-WebSocket-Version\", \"Sec-WebSocket-Extensions\",\n    };\n\n    private const int DefaultWebSocketBufferSize = 4096;\n    private readonly RequestDelegate _next;\n    private readonly IWebSocketsFactory _factory;\n\n    public const string IgnoredSslWarningFormat = $\"You have ignored all SSL warnings by using {nameof(DownstreamRoute.DangerousAcceptAnyServerCertificateValidator)} for this downstream route! {nameof(DownstreamRoute.UpstreamPathTemplate)}: '{{0}}', {nameof(DownstreamRoute.DownstreamPathTemplate)}: '{{1}}'.\";\n    public const string InvalidSchemeWarningFormat = \"Invalid scheme has detected which will be replaced! Scheme '{0}' of the downstream '{1}'.\";\n\n    public WebSocketsProxyMiddleware(IOcelotLoggerFactory logging,\n        RequestDelegate next,\n        IWebSocketsFactory factory)\n        : base(logging.CreateLogger<WebSocketsProxyMiddleware>())\n    {\n        _next = next;\n        _factory = factory;\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        var request = context.Items.DownstreamRequest();\n        var route = context.Items.DownstreamRoute();\n        await Proxy(context, request, route);\n    }\n\n    protected virtual async Task PumpAsync(WebSocket source, WebSocket destination, int bufferSize, CancellationToken cancellation)\n    {\n        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);\n        var buffer = new byte[bufferSize];\n        while (true)\n        {\n            WebSocketReceiveResult result = default;\n            try\n            {\n                result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellation);\n            }\n            catch (OperationCanceledException)\n            {\n                await TryCloseOutputAsync(destination, WebSocketCloseStatus.EndpointUnavailable, nameof(OperationCanceledException), cancellation);\n                return; // we don't rethrow timeout/cancellation errors\n            }\n            catch (WebSocketException e)\n            {\n                if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)\n                {\n                    await TryCloseOutputAsync(destination, WebSocketCloseStatus.EndpointUnavailable, $\"{nameof(WebSocketException)} when {nameof(e.WebSocketErrorCode)} is {nameof(WebSocketError.ConnectionClosedPrematurely)}\", cancellation);\n                }\n\n                // DON'T THROW, NEVER! Just log the warning...\n                // The logging level has been decreased from level 4 (Error) to level 3 (Warning) due to the high number of disconnecting events for sensitive WebSocket connections in unstable networks.\n                Logger.LogWarning(() => $\"{nameof(WebSocketException)} when {nameof(e.WebSocketErrorCode)} is {e.WebSocketErrorCode}\");\n                return; // swallow the error\n            }\n\n            if (result.MessageType == WebSocketMessageType.Close)\n            {\n                await TryCloseOutputAsync(destination, source.CloseStatus.Value, source.CloseStatusDescription, cancellation);\n                return;\n            }\n\n            if (destination.State == WebSocketState.Open)\n            {\n                await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellation);\n            }\n        }\n    }\n\n    private async Task Proxy(HttpContext context, DownstreamRequest request, DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(request);\n        ArgumentNullException.ThrowIfNull(route);\n\n        if (!context.WebSockets.IsWebSocketRequest)\n        {\n            throw new InvalidOperationException();\n        }\n\n        var client = _factory.CreateClient(); // new ClientWebSocket();\n        if (route.DangerousAcceptAnyServerCertificateValidator)\n        {\n            client.Options.RemoteCertificateValidationCallback = (request, certificate, chain, errors) => true;\n            Logger.LogWarning(() => string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate));\n        }\n\n        foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols)\n        {\n            client.Options.AddSubProtocol(protocol);\n        }\n\n        foreach (var header in context.Request.Headers)\n        {\n            if (!NotForwardedWebSocketHeaders.Contains(header.Key, StringComparer.OrdinalIgnoreCase))\n            {\n                try\n                {\n                    client.Options.SetRequestHeader(header.Key, header.Value);\n                }\n                catch (ArgumentException)\n                {\n                    // Expected in .NET Framework for headers that are mistakenly considered restricted.\n                    // See: https://github.com/dotnet/corefx/issues/26627\n                    // .NET Core does not exhibit this issue, ironically due to a separate bug (https://github.com/dotnet/corefx/issues/18784)\n                }\n            }\n        }\n\n        // Only Uris starting with 'ws://' or 'wss://' are supported in System.Net.WebSockets.ClientWebSocket\n        var scheme = request.Scheme;\n        if (!scheme.StartsWith(Uri.UriSchemeWs))\n        {\n            Logger.LogWarning(() => string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri()));\n            request.Scheme = scheme == Uri.UriSchemeHttp\n                ? Uri.UriSchemeWs\n                : scheme == Uri.UriSchemeHttps ? Uri.UriSchemeWss : scheme;\n        }\n\n        var destinationUri = new Uri(request.ToUri());\n        await client.ConnectAsync(destinationUri, context.RequestAborted);\n\n        using var server = await context.WebSockets.AcceptWebSocketAsync(client.SubProtocol);\n        await Task.WhenAll(\n            PumpAsync(client.ToWebSocket(), server, DefaultWebSocketBufferSize, context.RequestAborted),\n            PumpAsync(server, client.ToWebSocket(), DefaultWebSocketBufferSize, context.RequestAborted));\n    }\n\n    /// <summary>\n    /// Closes the WebSocket only if its state is <see cref=\"WebSocketState.Open\"/> or <see cref=\"WebSocketState.CloseReceived\"/>.\n    /// </summary>\n    /// <returns>The underlying closing task if the <paramref name=\"webSocket\"/> <see cref=\"WebSocket.State\"/> matches; otherwise, the <see cref=\"Task.CompletedTask\"/>.</returns>\n    protected virtual Task TryCloseOutputAsync(WebSocket webSocket, WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellation)\n        => (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)\n            ? webSocket.CloseOutputAsync(closeStatus, statusDescription, cancellation)\n            : Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/Ocelot/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[12.1.1, )\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[6.3.0, )\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.32, )\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {},\n    \"net10.0/win-x64\": {},\n    \"net8.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[12.1.1, )\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[6.3.0, )\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.25, )\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.25, )\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.32, )\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.2\",\n        \"contentHash\": \"3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {},\n    \"net8.0/win-x64\": {},\n    \"net9.0\": {\n      \"FluentValidation\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[12.1.1, )\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[6.3.0, )\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[9.0.14, )\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[9.0.14, )\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.32, )\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {},\n    \"net9.0/win-x64\": {}\n  }\n}"
  },
  {
    "path": "src/Ocelot.Provider.Consul/Consul.cs",
    "content": "﻿using Ocelot.Logging;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Consul;\n\npublic class Consul : IServiceDiscoveryProvider\n{\n    private readonly ConsulRegistryConfiguration _configuration;\n    private readonly IConsulClient _consul;\n    private readonly IOcelotLogger _logger;\n    private readonly IConsulServiceBuilder _serviceBuilder;\n\n    public Consul(\n        ConsulRegistryConfiguration config,\n        IOcelotLoggerFactory factory,\n        IConsulClientFactory clientFactory,\n        IConsulServiceBuilder serviceBuilder)\n    {\n        _configuration = config;\n        _consul = clientFactory.Get(_configuration);\n        _logger = factory.CreateLogger<Consul>();\n        _serviceBuilder = serviceBuilder;\n    }\n\n    public virtual async Task<List<Service>> GetAsync()\n    {\n        var entriesTask = _consul.Health.Service(_configuration.KeyOfServiceInConsul, string.Empty, true);\n        var nodesTask = _consul.Catalog.Nodes();\n\n        await Task.WhenAll(entriesTask, nodesTask);\n        var entries = (await entriesTask).Response ?? Array.Empty<ServiceEntry>();\n        var nodes = (await nodesTask).Response ?? Array.Empty<Node>();\n        if (entries.Length == 0)\n        {\n            _logger.LogWarning(() => $\"{nameof(Consul)} Provider: No service entries found for '{_configuration.KeyOfServiceInConsul}' service!\");\n            return new();\n        }\n\n        _logger.LogDebug(() => $\"{nameof(Consul)} Provider: Found total {entries.Length} service entries for '{_configuration.KeyOfServiceInConsul}' service.\");\n        _logger.LogDebug(() => $\"{nameof(Consul)} Provider: Found total {nodes.Length} catalog nodes.\");\n        return BuildServices(entries, nodes)\n            .ToList();\n    }\n\n    protected virtual IEnumerable<Service> BuildServices(ServiceEntry[] entries, Node[] nodes)\n        => _serviceBuilder.BuildServices(entries, nodes);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/ConsulClientFactory.cs",
    "content": "﻿using Ocelot.Provider.Consul.Interfaces;\n\nnamespace Ocelot.Provider.Consul;\n\npublic class ConsulClientFactory : IConsulClientFactory\n{\n    // TODO We need this overloaded method -> \n    //public IConsulClient Get(ServiceProviderConfiguration config)\n    public IConsulClient Get(ConsulRegistryConfiguration config)\n        => new ConsulClient(c => OverrideConfig(c, config));\n\n    // TODO ->\n    //private static void OverrideConfig(ConsulClientConfiguration to, ServiceProviderConfiguration from)\n    // Factory which consumes concrete types is a bad factory! A more abstract types are required\n    private static void OverrideConfig(ConsulClientConfiguration to, ConsulRegistryConfiguration from) // TODO Why ConsulRegistryConfiguration? We use ServiceProviderConfiguration props only! :)\n    {\n        to.Address = new Uri($\"{from.Scheme}://{from.Host}:{from.Port}\");\n\n        if (!string.IsNullOrEmpty(from?.Token))\n        {\n            to.Token = from.Token;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs",
    "content": "﻿using Microsoft.Extensions.Options;\nusing Newtonsoft.Json;\nusing Ocelot.Cache;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.Responses;\nusing System.Text;\n\nnamespace Ocelot.Provider.Consul;\n\npublic class ConsulFileConfigurationRepository : IFileConfigurationRepository\n{\n    private readonly IOcelotCache<FileConfiguration> _cache;\n    private readonly string _configurationKey;\n    private readonly IConsulClient _consul;\n    private readonly IOcelotLogger _logger;\n\n    public ConsulFileConfigurationRepository(\n        IOptions<FileConfiguration> fileConfiguration,\n        IOcelotCache<FileConfiguration> cache,\n        IConsulClientFactory factory,\n        IOcelotLoggerFactory loggerFactory)\n    {\n        _logger = loggerFactory.CreateLogger<ConsulFileConfigurationRepository>();\n        _cache = cache;\n\n        var provider = fileConfiguration.Value.GlobalConfiguration.ServiceDiscoveryProvider;\n        _configurationKey = string.IsNullOrWhiteSpace(provider.ConfigurationKey)\n            ? nameof(InternalConfiguration)\n            : provider.ConfigurationKey;\n\n        var config = new ConsulRegistryConfiguration(provider.Scheme, provider.Host,\n            provider.Port, _configurationKey, provider.Token);\n        _consul = factory.Get(config);\n    }\n\n    public async Task<Response<FileConfiguration>> Get()\n    {\n        var config = _cache.Get(_configurationKey, _configurationKey);\n        if (config != null)\n        {\n            return new OkResponse<FileConfiguration>(config);\n        }\n\n        var queryResult = await _consul.KV.Get(_configurationKey);\n        if (queryResult.Response == null)\n        {\n            return new OkResponse<FileConfiguration>(null);\n        }\n\n        var bytes = queryResult.Response.Value;\n        var json = Encoding.UTF8.GetString(bytes);\n        var consulConfig = JsonConvert.DeserializeObject<FileConfiguration>(json);\n\n        return new OkResponse<FileConfiguration>(consulConfig);\n    }\n\n    public async Task<Response> Set(FileConfiguration ocelotConfiguration)\n    {\n        var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented);\n        var bytes = Encoding.UTF8.GetBytes(json);\n        var kvPair = new KVPair(_configurationKey)\n        {\n            Value = bytes,\n        };\n\n        var result = await _consul.KV.Put(kvPair);\n        if (result.Response)\n        {\n            _cache.AddOrUpdate(_configurationKey, ocelotConfiguration, _configurationKey, TimeSpan.FromSeconds(3));\n            return new OkResponse();\n        }\n\n        return new ErrorResponse(new UnableToSetConfigInConsulError(\n            $\"Unable to set {nameof(FileConfiguration)} in {nameof(Consul)}, response status code from {nameof(Consul)} was {result.StatusCode}\"));\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.Provider.Consul;\n\npublic static class ConsulMiddlewareConfigurationProvider\n{\n    public static OcelotMiddlewareConfigurationDelegate Get { get; } = GetAsync;\n\n    private static async Task GetAsync(IApplicationBuilder builder)\n    {\n        var fileConfigRepo = builder.ApplicationServices.GetService<IFileConfigurationRepository>();\n        var fileConfig = builder.ApplicationServices.GetService<IOptionsMonitor<FileConfiguration>>();\n        var internalConfigCreator = builder.ApplicationServices.GetService<IInternalConfigurationCreator>();\n        var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();\n\n        if (UsingConsul(fileConfigRepo))\n        {\n            await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo);\n        }\n    }\n\n    private static bool UsingConsul(IFileConfigurationRepository fileConfigRepo)\n        => fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository);\n\n    private static async Task SetFileConfigInConsul(IApplicationBuilder builder,\n        IFileConfigurationRepository fileConfigRepo, IOptionsMonitor<FileConfiguration> fileConfig,\n        IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo)\n    {\n        // Get the config from Consul\n        var fileConfigFromConsul = await fileConfigRepo.Get();\n        if (IsError(fileConfigFromConsul))\n        {\n            ThrowToStopOcelotStarting(fileConfigFromConsul);\n        }\n        else if (ConfigNotStoredInConsul(fileConfigFromConsul))\n        {\n            // there was no config in Consul set the file in config in Consul\n            await fileConfigRepo.Set(fileConfig.CurrentValue);\n        }\n        else\n        {\n            // Create the internal config from Consul data\n            var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data);\n            if (IsError(internalConfig))\n            {\n                ThrowToStopOcelotStarting(internalConfig);\n            }\n            else\n            {\n                // add the internal config to the internal repo\n                var response = internalConfigRepo.AddOrReplace(internalConfig.Data);\n                if (IsError(response))\n                {\n                    ThrowToStopOcelotStarting(response);\n                }\n            }\n\n            if (IsError(internalConfig))\n            {\n                ThrowToStopOcelotStarting(internalConfig);\n            }\n        }\n    }\n\n    private static void ThrowToStopOcelotStarting(Response config)\n        => throw new Exception($\"Unable to start Ocelot, errors are:{config.Errors.ToErrorString(true, true)}\");\n\n    private static bool IsError(Response response)\n        => response == null || response.IsError;\n\n    private static bool ConfigNotStoredInConsul(Response<FileConfiguration> fileConfigFromConsul)\n        => fileConfigFromConsul.Data == null;\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/ConsulProviderFactory.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.Provider.Consul;\n\n/// <summary>\n/// TODO It must be refactored converting to real factory-class and add to DI.\n/// </summary>\n/// <remarks>\n/// Must inherit from <see cref=\"IServiceDiscoveryProviderFactory\"/> interface.\n/// Also the <see cref=\"ServiceDiscoveryFinderDelegate\"/> must be removed from the design.\n/// </remarks>\npublic static class ConsulProviderFactory // TODO : IServiceDiscoveryProviderFactory\n{\n    /// <summary>String constant used for provider type definition.</summary>\n    public const string PollConsul = nameof(Provider.Consul.PollConsul);\n\n    private static readonly List<PollConsul> ServiceDiscoveryProviders = new(); // TODO It must be singleton service in DI-container\n#if NET9_0_OR_GREATER\n    private static readonly System.Threading.Lock SyncRoot = new();\n#else\n    private static readonly object SyncRoot = new();\n#endif\n\n    public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider;\n    private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route)\n    {\n        // Singleton services\n        var factory = provider.GetService<IOcelotLoggerFactory>();\n        var consulFactory = provider.GetService<IConsulClientFactory>();\n        var contextAccessor = provider.GetService<IHttpContextAccessor>();\n\n        // Scoped services\n        var context = contextAccessor.HttpContext;\n        var configuration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); // TODO Why not to pass 2 args only: config, route? LoL\n        context.Items[nameof(ConsulRegistryConfiguration)] = configuration; // initialize data\n        var serviceBuilder = context.RequestServices.GetService<IConsulServiceBuilder>(); // consume data in default/custom builder\n\n        var consulProvider = new Consul(configuration, factory, consulFactory, serviceBuilder); // TODO It must be added to DI-container!\n\n        if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase))\n        {\n            lock (SyncRoot)\n            {\n                var discoveryProvider = ServiceDiscoveryProviders.FirstOrDefault(x => x.ServiceName == route.ServiceName);\n                if (discoveryProvider != null)\n                {\n                    return discoveryProvider;\n                }\n\n                discoveryProvider = new PollConsul(config.PollingInterval, route.ServiceName, factory, consulProvider);\n                ServiceDiscoveryProviders.Add(discoveryProvider);\n                return discoveryProvider;\n            }\n        }\n\n        return consulProvider;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs",
    "content": "﻿namespace Ocelot.Provider.Consul;\n\npublic class ConsulRegistryConfiguration // TODO Inherit from ServiceProviderConfiguration ?\n{\n    /// <summary>\n    /// Consul HTTP client default port.\n    /// <para>\n    /// HashiCorp Developer docs: <see href=\"https://developer.hashicorp.com/consul\">Consul</see> | <see href=\"https://developer.hashicorp.com/consul/docs/install/ports\">Required Ports</see>.\n    /// </para>\n    /// </summary>\n    public const int DefaultHttpPort = 8500;\n\n    public ConsulRegistryConfiguration(string scheme, string host, int port, string keyOfServiceInConsul, string token)\n    {\n        // TODO Why not to encapsulate this biz logic right in ConsulProviderFactory? LoL\n        Host = string.IsNullOrEmpty(host) ? \"localhost\" : host;\n        Port = port > 0 ? port : DefaultHttpPort;\n        Scheme = string.IsNullOrEmpty(scheme) ? Uri.UriSchemeHttp : scheme;\n        KeyOfServiceInConsul = keyOfServiceInConsul;\n        Token = token;\n    }\n\n    public string KeyOfServiceInConsul { get; }\n    public string Scheme { get; }\n    public string Host { get; }\n    public int Port { get; }\n    public string Token { get; }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Consul;\n\npublic class DefaultConsulServiceBuilder : IConsulServiceBuilder\n{\n    private readonly HttpContext _context;\n    private readonly IConsulClientFactory _clientFactory;\n    private readonly IOcelotLoggerFactory _loggerFactory;\n\n    private ConsulRegistryConfiguration _configuration;\n    private IConsulClient _client;\n    private IOcelotLogger _logger;\n\n    public DefaultConsulServiceBuilder(\n        IHttpContextAccessor contextAccessor,\n        IConsulClientFactory clientFactory,\n        IOcelotLoggerFactory loggerFactory)\n    {\n        _context = contextAccessor.HttpContext;\n        _clientFactory = clientFactory;\n        _loggerFactory = loggerFactory;\n    }\n\n    // TODO See comment in the interface about the privacy. The goal is to eliminate IBC!\n    // So, we need more abstract type, and ServiceProviderConfiguration is a good choice. The rest of props can be obtained from HttpContext\n    protected /*public*/ ConsulRegistryConfiguration Configuration => _configuration\n        ??= _context.Items.TryGetValue(nameof(ConsulRegistryConfiguration), out var value)\n            ? value as ConsulRegistryConfiguration : default;\n    protected IConsulClient Client => _client ??= _clientFactory.Get(Configuration);\n    protected IOcelotLogger Logger => _logger ??= _loggerFactory.CreateLogger<DefaultConsulServiceBuilder>();\n\n    public virtual bool IsValid(ServiceEntry entry)\n    {\n        var service = entry.Service;\n        var address = service.Address;\n        bool valid = !string.IsNullOrEmpty(address)\n            && !address.StartsWith(Uri.UriSchemeHttp + \"://\", StringComparison.OrdinalIgnoreCase)\n            && !address.StartsWith(Uri.UriSchemeHttps + \"://\", StringComparison.OrdinalIgnoreCase)\n            && service.Port > 0;\n\n        if (!valid)\n        {\n            Logger.LogWarning(\n                () => $\"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0.\");\n        }\n\n        return valid;\n    }\n\n    public virtual IEnumerable<Service> BuildServices(ServiceEntry[] entries, Node[] nodes)\n    {\n        ArgumentNullException.ThrowIfNull(entries);\n        var services = new List<Service>(entries.Length);\n\n        foreach (var serviceEntry in entries)\n        {\n            if (IsValid(serviceEntry))\n            {\n                var serviceNode = GetNode(serviceEntry, nodes);\n                var item = CreateService(serviceEntry, serviceNode);\n                if (item != null)\n                {\n                    services.Add(item);\n                }\n            }\n        }\n\n        return services;\n    }\n\n    protected virtual Node GetNode(ServiceEntry entry, Node[] nodes)\n        => entry?.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address);\n\n    public virtual Service CreateService(ServiceEntry entry, Node node)\n        => new(\n            GetServiceName(entry, node),\n            GetServiceHostAndPort(entry, node),\n            GetServiceId(entry, node),\n            GetServiceVersion(entry, node),\n            GetServiceTags(entry, node)\n        );\n\n    protected virtual string GetServiceName(ServiceEntry entry, Node node)\n        => entry.Service.Service;\n\n    protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node)\n        => new(\n            GetDownstreamHost(entry, node),\n            entry.Service.Port);\n\n    protected virtual string GetDownstreamHost(ServiceEntry entry, Node node)\n        => node != null ? node.Name : entry.Service.Address;\n\n    protected virtual string GetServiceId(ServiceEntry entry, Node node)\n        => entry.Service.ID;\n\n    protected virtual string GetServiceVersion(ServiceEntry entry, Node node)\n        => entry.Service.Tags\n            ?.FirstOrDefault(tag => tag.StartsWith(VersionPrefix, StringComparison.Ordinal))\n            ?.TrimPrefix(VersionPrefix)\n            ?? string.Empty;\n\n    protected virtual IEnumerable<string> GetServiceTags(ServiceEntry entry, Node node)\n        => entry.Service.Tags ?? Enumerable.Empty<string>();\n\n    private const string VersionPrefix = \"version-\";\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs",
    "content": "﻿namespace Ocelot.Provider.Consul.Interfaces;\n\npublic interface IConsulClientFactory\n{\n    IConsulClient Get(ConsulRegistryConfiguration config);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs",
    "content": "﻿using Ocelot.Values;\n\nnamespace Ocelot.Provider.Consul.Interfaces;\n\npublic interface IConsulServiceBuilder\n{\n    // Keep config private (deep encapsulation) until an architectural decision is made.\n    // ConsulRegistryConfiguration Configuration { get; }\n    bool IsValid(ServiceEntry entry);\n    IEnumerable<Service> BuildServices(ServiceEntry[] entries, Node[] nodes);\n    Service CreateService(ServiceEntry serviceEntry, Node serviceNode);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <NoPackageAnalysis>true</NoPackageAnalysis>\n    <Description>Provides Ocelot extensions to use Consul service discovery.</Description>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyName>Ocelot.Provider.Consul</AssemblyName>\n    <PackageId>Ocelot.Provider.Consul</PackageId>\n    <PackageTags>API Gateway;.NET;Consul</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Consul</PackageProjectUrl>\n    <PackageIconUrl>https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png</PackageIconUrl>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <Authors>Tom Pallister, Raman Maksimchuk, Guillaume Gnaegi</Authors>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <NoWarn>1591</NoWarn>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2025 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"..\\..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ocelot\\Ocelot.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Consul\" Version=\"1.7.14.10\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Consul.Interfaces;\n\nnamespace Ocelot.Provider.Consul;\n\npublic static class OcelotBuilderExtensions\n{\n    /// <summary>\n    /// Integrates Consul service discovery into the DI, atop the existing Ocelot services.\n    /// </summary>\n    /// <remarks>\n    /// Default services:\n    /// <list type=\"bullet\">\n    /// <item>The <see cref=\"IConsulClientFactory\"/> service is an instance of <see cref=\"ConsulClientFactory\"/>.</item>\n    /// <item>The <see cref=\"IConsulServiceBuilder\"/> service is an instance of <see cref=\"DefaultConsulServiceBuilder\"/>.</item>\n    /// </list>\n    /// </remarks>\n    /// <param name=\"builder\">The Ocelot Builder instance, default.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddConsul(this IOcelotBuilder builder)\n    {\n        builder.Services\n            .AddSingleton(ConsulProviderFactory.Get)\n            .AddSingleton<IConsulClientFactory, ConsulClientFactory>()\n            .AddScoped<IConsulServiceBuilder, DefaultConsulServiceBuilder>()\n            .RemoveAll(typeof(IFileConfigurationPollerOptions))\n            .AddSingleton<IFileConfigurationPollerOptions, ConsulFileConfigurationPollerOption>();\n        return builder;\n    }\n\n    /// <summary>\n    /// Integrates Consul service discovery into the DI, atop the existing Ocelot services, with service builder overriding.\n    /// </summary>\n    /// <remarks>\n    /// Services to override:\n    /// <list type=\"bullet\">\n    /// <item>The <see cref=\"IConsulServiceBuilder\"/> service has been substituted with a <typeparamref name=\"TServiceBuilder\"/> instance.</item>\n    /// </list>\n    /// </remarks>\n    /// <typeparam name=\"TServiceBuilder\">The service builder type.</typeparam>\n    /// <param name=\"builder\">The Ocelot Builder instance, default.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddConsul<TServiceBuilder>(this IOcelotBuilder builder)\n        where TServiceBuilder : class, IConsulServiceBuilder\n    {\n        AddConsul(builder).Services\n            .RemoveAll<IConsulServiceBuilder>()\n            .AddScoped(typeof(IConsulServiceBuilder), typeof(TServiceBuilder));\n        return builder;\n    }\n\n    public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder)\n    {\n        builder.Services\n            .AddSingleton(ConsulMiddlewareConfigurationProvider.Get)\n            .AddHostedService<FileConfigurationPoller>()\n            .AddSingleton<IFileConfigurationRepository, ConsulFileConfigurationRepository>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/PollConsul.cs",
    "content": "﻿using Ocelot.Logging;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Consul;\n\npublic sealed class PollConsul : IServiceDiscoveryProvider\n{\n    private readonly object _lockObject = new();\n    private readonly IOcelotLogger _logger;\n    private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider;\n    private readonly int _pollingInterval;\n\n    private DateTime _lastUpdateTime;\n    private List<Service> _services;\n\n    public PollConsul(int pollingInterval, string serviceName, IOcelotLoggerFactory factory,\n        IServiceDiscoveryProvider consulServiceDiscoveryProvider)\n    {\n        _logger = factory.CreateLogger<PollConsul>();\n        _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider;\n        _pollingInterval = pollingInterval;\n\n        // Initialize by DateTime.MinValue as lowest value.\n        // Polling will occur immediately during the first call\n        _lastUpdateTime = DateTime.MinValue;\n\n        _services = new List<Service>();\n        ServiceName = serviceName;\n    }\n\n    public string ServiceName { get; }\n\n    /// <summary>\n    /// Gets the services.\n    /// <para>If the first call, retrieves the services and then starts the timer.</para>\n    /// </summary>\n    /// <returns>A <see cref=\"Task{T}\" /> with a <see cref=\"List{Service}\" /> result of <see cref=\"Service\" />.</returns>\n    public Task<List<Service>> GetAsync()\n    {\n        lock (_lockObject)\n        {\n            var refreshTime = _lastUpdateTime.AddMilliseconds(_pollingInterval);\n\n            // Check if any services available\n            if (refreshTime >= DateTime.UtcNow && _services.Any())\n            {\n                return Task.FromResult(_services);\n            }\n\n            try\n            {\n                _logger.LogInformation(() => $\"Retrieving new client information for service: {ServiceName}...\");\n                _services = _consulServiceDiscoveryProvider.GetAsync().GetAwaiter().GetResult();\n                return Task.FromResult(_services);\n            }\n            finally\n            {\n                _lastUpdateTime = DateTime.UtcNow;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs",
    "content": "﻿using Ocelot.Errors;\nusing HttpStatus = System.Net.HttpStatusCode;\n\nnamespace Ocelot.Provider.Consul;\n\npublic class UnableToSetConfigInConsulError : Error\n{\n    public UnableToSetConfigInConsulError(string s)\n        : base(s, OcelotErrorCode.UnknownError, (int)HttpStatus.NotFound)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using Consul;\nglobal using Ocelot.ServiceDiscovery;\n"
  },
  {
    "path": "src/Ocelot.Provider.Consul/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {},\n    \"net10.0/win-x64\": {},\n    \"net8.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.2\",\n        \"contentHash\": \"3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {},\n    \"net8.0/win-x64\": {},\n    \"net9.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {},\n    \"net9.0/win-x64\": {}\n  }\n}"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/Eureka.cs",
    "content": "﻿using Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\r\nnamespace Ocelot.Provider.Eureka;\r\n\r\npublic class Eureka : IServiceDiscoveryProvider\r\n{\r\n    private readonly string _serviceName;\r\n    private readonly IDiscoveryClient _client;\r\n\r\n    public Eureka(string serviceName, IDiscoveryClient client)\r\n    {\r\n        _serviceName = serviceName ?? throw new ArgumentNullException(nameof(serviceName));\r\n        _client = client ?? throw new ArgumentNullException(nameof(client));\r\n    }\r\n\r\n    public Task<List<Service>> GetAsync()\n    {\r\n        var services = new List<Service>();\r\n\r\n        var instances = _client.GetInstances(_serviceName);\r\n        if (instances != null && instances.Any())\r\n        {\r\n            services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port, i.Uri.Scheme), string.Empty, string.Empty, new List<string>())));\r\n        }\r\n\r\n        return Task.FromResult(services);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Provider.Eureka;\n\npublic class EurekaMiddlewareConfigurationProvider\n{\n    public static OcelotMiddlewareConfigurationDelegate Get { get; } = builder =>\n    {\n        var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();\n\n        var config = internalConfigRepo.Get();\n\n        if (UsingEurekaServiceDiscoveryProvider(config.Data))\n        {\n            //builder.UseDiscoveryClient();\n        }\n\n        return Task.CompletedTask;\n    };\n\n    private static bool UsingEurekaServiceDiscoveryProvider(IInternalConfiguration configuration)\n    {\n        return configuration?.ServiceProviderConfiguration != null && configuration.ServiceProviderConfiguration.Type?.ToLower() == \"eureka\";\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.Provider.Eureka;\n\npublic static class EurekaProviderFactory\n{\n    /// <summary>\n    /// String constant used for provider type definition.\n    /// </summary>\n    public const string Eureka = nameof(Provider.Eureka.Eureka);\n\n    public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider;\n\n    private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route)\n    {\n        var client = provider.GetService<IDiscoveryClient>();\n        if (client == null)\n        {\n            throw new NullReferenceException($\"Cannot get an {nameof(IDiscoveryClient)} service during {nameof(CreateProvider)} operation to instanciate the {nameof(Eureka)} provider!\");\n        }\n\n        return Eureka.Equals(config.Type, StringComparison.OrdinalIgnoreCase)\n            ? new Eureka(route.ServiceName, client)\n            : null;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <NoPackageAnalysis>true</NoPackageAnalysis>\n    <Description>Provides Ocelot extensions to use Netflix Eureka service discovery.</Description>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyName>Ocelot.Provider.Eureka</AssemblyName>\n    <PackageId>Ocelot.Provider.Eureka</PackageId>\n    <PackageTags>API Gateway;.NET;Netflix;Eureka</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Eureka</PackageProjectUrl>\n    <PackageIconUrl>https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png</PackageIconUrl>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <NoWarn>1591</NoWarn>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"..\\..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ocelot\\Ocelot.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Steeltoe.Discovery.ClientCore\" Version=\"3.3.0\" /><!-- TODO Requires upgrade to v4.0 after package upgraded -->\n    <PackageReference Include=\"Steeltoe.Discovery.Eureka\" Version=\"3.3.0\" /><!-- TODO Requires upgrade to v4.0 after package upgraded -->\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Steeltoe.Discovery.Client;\n\nnamespace Ocelot.Provider.Eureka;\n\npublic static class OcelotBuilderExtensions\n{\n    public static IOcelotBuilder AddEureka(this IOcelotBuilder builder)\n    {\n        builder.Services\n            .AddDiscoveryClient(builder.Configuration)\n            .AddSingleton(EurekaProviderFactory.Get)\n            .AddSingleton(EurekaMiddlewareConfigurationProvider.Get);\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using Ocelot;\nglobal using Ocelot.ServiceDiscovery;\nglobal using Steeltoe.Discovery;\nglobal using Steeltoe.Discovery.Eureka;\n"
  },
  {
    "path": "src/Ocelot.Provider.Eureka/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    },\n    \"net10.0/win-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    },\n    \"net8.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.2\",\n        \"contentHash\": \"3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    },\n    \"net8.0/win-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    },\n    \"net9.0\": {\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.0\",\n        \"contentHash\": \"DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"3.1.0\",\n          \"Microsoft.Extensions.Logging\": \"3.1.0\",\n          \"Microsoft.Extensions.Options\": \"3.1.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    },\n    \"net9.0/win-x64\": {\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs",
    "content": "﻿using KubeClient.Http;\nusing KubeClient.Models;\nusing KubeClient.ResourceClients;\nusing Ocelot.Provider.Kubernetes.Interfaces;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic class EndPointClientV1 : KubeResourceClient, IEndPointClient\n{\n    private static readonly HttpRequest EndpointsRequest =\n        KubeRequest.Create(\"api/v1/namespaces/{Namespace}/endpoints/{ServiceName}\");\n\n    private static readonly HttpRequest EndpointsWatchRequest =\n        KubeRequest.Create(\"api/v1/watch/namespaces/{Namespace}/endpoints/{ServiceName}\");\n\n    public EndPointClientV1(IKubeApiClient client) : base(client)\n    {\n    }\n\n    public Task<EndpointsV1> GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(serviceName);\n\n        var request = EndpointsRequest.WithTemplateParameters(new\n        {\n            Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,\n            ServiceName = serviceName,\n        });\n\n        return Http.GetAsync(request, cancellationToken)\n            .ReadContentAsObjectV1Async<EndpointsV1>(operationDescription: $\"{nameof(GetAsync)} {nameof(EndpointsV1)}\");\n    }\n\n    public IObservable<IResourceEventV1<EndpointsV1>> Watch(string serviceName, string kubeNamespace, CancellationToken cancellationToken = default)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(serviceName);\n\n        var request = EndpointsWatchRequest.WithTemplateParameters(new\n        {\n            ServiceName = serviceName,\n            Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,\n        });\n\n        return ObserveEvents<EndpointsV1>(request,\n            $\"{nameof(Watch)} {nameof(EndpointsV1)} for '{serviceName}' in the namespace '{kubeNamespace ?? KubeClient.DefaultNamespace}'\");\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs",
    "content": "﻿using KubeClient.Models;\nusing KubeClient.ResourceClients;\n\nnamespace Ocelot.Provider.Kubernetes.Interfaces;\n\npublic interface IEndPointClient : IKubeResourceClient\n{\n    Task<EndpointsV1> GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default);\n\n    IObservable<IResourceEventV1<EndpointsV1>> Watch(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Interfaces/IKubeApiClientFactory.cs",
    "content": "﻿namespace Ocelot.Provider.Kubernetes.Interfaces;\n\npublic interface IKubeApiClientFactory\n{\n    KubeApiClient Get(bool usePodServiceAccount);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Kubernetes.Interfaces;\n\npublic interface IKubeServiceBuilder\n{\n    IEnumerable<Service> BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Kubernetes.Interfaces;\n\npublic interface IKubeServiceCreator\n{\n    IEnumerable<Service> Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset);\n    IEnumerable<Service> CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Kube.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Infrastructure.DesignPatterns;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Kubernetes;\n\n/// <summary>Default Kubernetes service discovery provider.</summary>\n/// <remarks>\n/// <list type=\"bullet\">\n/// <item>NuGet: <see href=\"https://www.nuget.org/packages/KubeClient\">KubeClient</see></item>\n/// <item>GitHub: <see href=\"https://github.com/tintoy/dotnet-kube-client\">dotnet-kube-client</see></item>\n/// </list>\n/// </remarks>\npublic class Kube : IServiceDiscoveryProvider, IDisposable\n{\n    private static readonly (string ResourceKind, string ResourceApiVersion) EndPointsKubeKind = KubeObjectV1.GetKubeKind<EndpointsV1>();\n\n    private readonly KubeRegistryConfiguration _configuration;\n    private readonly IOcelotLogger _logger;\n    private readonly IKubeApiClient _kubeApi;\n    private readonly IKubeServiceBuilder _serviceBuilder;\n    private bool _disposed;\n\n    public Kube(\n        KubeRegistryConfiguration configuration,\n        IOcelotLoggerFactory factory,\n        IKubeApiClient kubeApi,\n        IKubeServiceBuilder serviceBuilder)\n    {\n        _configuration = configuration;\n        _logger = factory.CreateLogger<Kube>();\n        _kubeApi = kubeApi;\n        _serviceBuilder = serviceBuilder;\n    }\n\n    public virtual async Task<List<Service>> GetAsync()\n    {\n        if (_disposed)\n            return new(0);\n\n        var endpoint = await Retry.OperationAsync(GetEndpoint, CheckErroneousState, logger: _logger);\n        if (CheckErroneousState(endpoint))\n        {\n            _logger.LogWarning(() => GetMessage($\"Unable to use bad result returned by {nameof(Kube)} integration endpoint because the final result is invalid/unknown after multiple retries!\"));\n            return new(0);\n        }\n\n        return BuildServices(_configuration, endpoint)\n            .ToList();\n    }\n\n    private string Message(string details)\n        => $\"Failed to retrieve {EndPointsKubeKind.ResourceApiVersion}/{EndPointsKubeKind.ResourceKind} '{_configuration.KeyOfServiceInK8s}' in namespace '{_configuration.KubeNamespace}': {details}\";\n\n    private async Task<EndpointsV1> GetEndpoint()\n    {\n        if (_disposed)\n            return null;\n\n        try\n        {\n            return await _kubeApi\n                .EndpointsV1()\n                .GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace);\n        }\n        catch (KubeApiException ex)\n        {\n            string Msg()\n            {\n                StatusV1 status = ex.Status;\n                string httpStatusCode = \"-\"; // Unknown\n                if (ex.InnerException is HttpRequestException e)\n                {\n                    httpStatusCode = e.StatusCode.ToString();\n                }\n\n                return Message($\"(HTTP.{httpStatusCode}/{status.Status}/{status.Reason}): {status.Message}\");\n            }\n\n            _logger.LogError(Msg, ex);\n        }\n        catch (HttpRequestException ex)\n        {\n            _logger.LogError(() => Message($\"({ex.HttpRequestError}/HTTP.{ex.StatusCode}).\"), ex);\n        }\n        catch (Exception unexpected)\n        {\n            _logger.LogError(() => Message($\"(an unexpected ex occurred).\"), unexpected);\n        }\n\n        return null;\n    }\n\n    private bool CheckErroneousState(EndpointsV1 endpoint)\n        => (endpoint?.Subsets?.Count ?? 0) == 0; // null or count is zero\n\n    private string GetMessage(string message)\n        => $\"{nameof(Kube)} provider. Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; {message}\";\n\n    protected virtual IEnumerable<Service> BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint)\n        => _disposed\n            ? Enumerable.Empty<Service>()\n            : _serviceBuilder.BuildServices(configuration, endpoint);\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n            return;\n\n        if (disposing)\n        {\n            _logger?.Dispose();\n            _kubeApi?.Dispose();\n        }\n\n        _disposed = true;\n    }\n\n    public void Dispose()\n    {\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubeApiClientExtensions.cs",
    "content": "﻿using Ocelot.Provider.Kubernetes.Interfaces;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic static class KubeApiClientExtensions\n{\n    public static IEndPointClient EndpointsV1(this IKubeApiClient client)\n        => client.ResourceClient<IEndPointClient>(x => new EndPointClientV1(x));\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubeApiClientFactory.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Provider.Kubernetes.Interfaces;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic class KubeApiClientFactory : IKubeApiClientFactory\n{\n    private readonly ILoggerFactory _logger;\n    private readonly IOptions<KubeClientOptions> _options;\n    private string _serviceAccountPath;\n\n    public KubeApiClientFactory(ILoggerFactory logger, IOptions<KubeClientOptions> options)\n    {\n        _logger = logger;\n        _options = options;\n        _serviceAccountPath = KubeClientConstants.DefaultServiceAccountPath;\n    }\n\n    protected string ServiceAccountPath\n    {\n        get => _serviceAccountPath;\n        set => _serviceAccountPath = string.IsNullOrEmpty(value) ? KubeClientConstants.DefaultServiceAccountPath : value;\n    }\n\n    public virtual KubeApiClient Get(bool usePodServiceAccount)\n    {\n        var options = usePodServiceAccount\n            ? KubeClientOptions.FromPodServiceAccount(ServiceAccountPath)\n            : _options.Value;\n        options.LoggerFactory = _logger;\n        return KubeApiClient.Create(options);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs",
    "content": "﻿namespace Ocelot.Provider.Kubernetes;\n\npublic class KubeRegistryConfiguration\n{\n    public string KubeNamespace { get; set; }\n    public string KeyOfServiceInK8s { get; set; }\n    public string Scheme { get; set; }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic class KubeServiceBuilder : IKubeServiceBuilder\n{\n    private readonly IOcelotLogger _logger;\n    private readonly IKubeServiceCreator _serviceCreator;\n\n    public KubeServiceBuilder(IOcelotLoggerFactory factory, IKubeServiceCreator serviceCreator)\n    {\n        ArgumentNullException.ThrowIfNull(factory);\n        _logger = factory.CreateLogger<KubeServiceBuilder>();\n\n        ArgumentNullException.ThrowIfNull(serviceCreator);\n        _serviceCreator = serviceCreator;\n    }\n\n    public virtual IEnumerable<Service> BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint)\n    {\n        ArgumentNullException.ThrowIfNull(configuration);\n        ArgumentNullException.ThrowIfNull(endpoint);\n\n        var services = endpoint.Subsets\n            .SelectMany(subset => _serviceCreator.Create(configuration, endpoint, subset))\n            .ToArray();\n\n        _logger.LogDebug(() => $\"K8s '{Check(endpoint.Kind)}:{Check(endpoint.ApiVersion)}:{Check(endpoint.Metadata?.Name)}' endpoint: Total built {services.Length} services.\");\n        return services;\n    }\n\n    private static string Check(string str) => string.IsNullOrEmpty(str) ? \"?\" : str;\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic class KubeServiceCreator : IKubeServiceCreator\n{\n    public KubeServiceCreator(IOcelotLoggerFactory factory)\n    {\n        ArgumentNullException.ThrowIfNull(factory);\n        Logger = factory.CreateLogger<KubeServiceCreator>();\n    }\n\n    public virtual IEnumerable<Service> Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset)\n        => (configuration == null || endpoint == null || subset == null)\n            ? Array.Empty<Service>()\n            : subset.Addresses\n                .SelectMany(address => CreateInstance(configuration, endpoint, subset, address))\n                .ToArray();\n\n    public virtual IEnumerable<Service> CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n    {\n        var instance = new Service(\n            GetServiceName(configuration, endpoint, subset, address),\n            GetServiceHostAndPort(configuration, endpoint, subset, address),\n            GetServiceId(configuration, endpoint, subset, address),\n            GetServiceVersion(configuration, endpoint, subset, address),\n            GetServiceTags(configuration, endpoint, subset, address)\n        );\n        return new Service[] { instance };\n    }\n\n    protected IOcelotLogger Logger { get; }\n\n    protected virtual string GetServiceName(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n        => endpoint.Metadata?.Name;\n\n    protected virtual ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n    {\n        var ports = subset.Ports;\n        bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.OrdinalIgnoreCase);\n        var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme)\n            ? ports.FirstOrDefault()\n            : ports.FirstOrDefault(portNameToScheme);\n        portV1 ??= new();\n        portV1.Name ??= configuration.Scheme ?? string.Empty;\n        Logger.LogDebug(() => $\"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}].\");\n        return new ServiceHostAndPort(address.Ip, portV1.Port ?? 80, portV1.Name);\n    }\n\n    protected virtual string GetServiceId(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n        => endpoint.Metadata?.Uid;\n    protected virtual string GetServiceVersion(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n        => endpoint.ApiVersion;\n    protected virtual IEnumerable<string> GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n        => Enumerable.Empty<string>();\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Provider.Kubernetes.Interfaces;\r\n//using System.Collections.Concurrent;\r\nusing System.Reactive.Concurrency;\r\n\r\nnamespace Ocelot.Provider.Kubernetes;\r\n\r\npublic static class KubernetesProviderFactory // TODO : IServiceDiscoveryProviderFactory\r\n{\r\n    /// <summary>String constant used for provider type definition.</summary>\r\n    public const string PollKube = nameof(Kubernetes.PollKube);\r\n    public const string WatchKube = nameof(Kubernetes.WatchKube);\r\n\r\n    // private static readonly ConcurrentDictionary<string, IServiceDiscoveryProvider> _providers = new(); // TODO It must be singleton service in DI-container\r\n    public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider;\r\n\r\n    private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route)\r\n    {\r\n        //if (_providers.TryGetValue(route.LoadBalancerKey, out var instance)) // ?? route.ServiceName ??\r\n        //    return instance;\r\n        var factory = provider.GetService<IOcelotLoggerFactory>();\r\n        var kubeClient = provider.GetService<IKubeApiClient>();\r\n        var serviceBuilder = provider.GetService<IKubeServiceBuilder>();\r\n        var configuration = new KubeRegistryConfiguration\r\n        {\r\n            KeyOfServiceInK8s = route.ServiceName,\r\n            KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace,\r\n            Scheme = route.DownstreamScheme,\r\n        };\r\n\r\n        if (WatchKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase))\r\n        {\r\n            //return _providers.GetOrAdd(route.LoadBalancerKey,\r\n            //    key => new WatchKube(configuration, factory, kubeClient, serviceBuilder, Scheduler.Default));\r\n            return new WatchKube(configuration, factory, kubeClient, serviceBuilder, Scheduler.Default);\r\n        }\r\n\r\n        var kubeProvider = new Kube(configuration, factory, kubeClient, serviceBuilder);\r\n        return /*_providers.GetOrAdd(route.LoadBalancerKey,\r\n            key =>*/ PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase)\r\n                ? new PollKube(config.PollingInterval, factory, kubeProvider)\r\n                : kubeProvider; //);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/ObservableExtensions.cs",
    "content": "﻿using System.Reactive.Concurrency;\nusing System.Reactive.Linq;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic static class ObservableExtensions\n{\n    public static IObservable<TSource> RetryAfter<TSource>(this IObservable<TSource> source, TimeSpan dueTime, IScheduler scheduler)\n        => RepeatInfinite(source, dueTime, scheduler).Catch();\n\n    private static IEnumerable<IObservable<TSource>> RepeatInfinite<TSource>(IObservable<TSource> source, TimeSpan dueTime, IScheduler scheduler)\n    {\n        yield return source;\n        while (true)\n        {\n            yield return source.DelaySubscription(dueTime, scheduler);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <NoPackageAnalysis>true</NoPackageAnalysis>\n    <Product>Ocelot Gateway</Product>\n    <Description>Provides Ocelot extensions to use Kubernetes service discovery.</Description>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <PackageId>Ocelot.Provider.Kubernetes</PackageId>\n    <PackageTags>API Gateway;.NET;kubernetes</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Kubernetes</PackageProjectUrl>\n    <PackageIconUrl>https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png</PackageIconUrl>\n    <PackageReleaseNotes></PackageReleaseNotes>\n    <AssemblyName>Ocelot.Provider.Kubernetes</AssemblyName>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <NoWarn>1591</NoWarn>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"..\\..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"KubeClient\" Version=\"3.1.1\" />\n    <PackageReference Include=\"KubeClient.Extensions.DependencyInjection\" Version=\"3.1.1\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ocelot\\Ocelot.csproj\" />\n    <InternalsVisibleTo Include=\"Ocelot.UnitTests\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing System.Net;\nusing System.Reflection;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic static class OcelotBuilderExtensions\n{\n    /// <summary>\n    /// Adds Kubernetes (K8s) services with or without using a pod service account.\n    /// <para>By default, <paramref name=\"usePodServiceAccount\"/> is set to <see langword=\"true\"/>, which means using a pod service account.</para>\n    /// </summary>\n    /// <remarks>If <paramref name=\"usePodServiceAccount\"/> is <see langword=\"false\"/>, it internally injects an <see cref=\"IOptions{T}\"/> configuration section object (where TOptions is <see cref=\"KubeClientOptions\"/>) to configure <see cref=\"KubeApiClient\"/>.</remarks>\n    /// <param name=\"builder\">The Ocelot Builder instance.</param>\n    /// <param name=\"usePodServiceAccount\">If true, it creates the client from pod service account.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true)\n    {\n        builder.Services\n\n            //.AddSingleton<IKubeApiClient, KubeApiClient>(KubeApiClientFactory) // TODO Revert to .AddKubeClient(usePodServiceAccount) after making KubernetesProviderFactory as IServiceDiscoveryProviderFactory \n            //.AddKubeApiClientFactory(usePodServiceAccount)\n            //.AddKubeClient(usePodServiceAccount)\n            .AddSingleton<IKubeApiClientFactory, KubeApiClientFactory>()\n            .AddSingleton<IKubeApiClient, KubeApiClient>(ResolveWithKubeApiClientFactory)\n\n            .AddSingleton(KubernetesProviderFactory.Get) // TODO Must be removed after deprecation of ServiceDiscoveryFinderDelegate in favor of IServiceDiscoveryProviderFactory\n            .AddSingleton<IKubeServiceBuilder, KubeServiceBuilder>()\n            .AddSingleton<IKubeServiceCreator, KubeServiceCreator>();\n        return builder;\n\n        KubeApiClient ResolveWithKubeApiClientFactory(IServiceProvider sp)\n        {\n            var factory = sp.GetService<IKubeApiClientFactory>();\n            return factory.Get(usePodServiceAccount);\n        }\n    }\n\n    /// <summary>\n    /// Adds Kubernetes (K8s) services without using a pod service account, explicitly calling a factory-action to initialize configuration options for <see cref=\"KubeApiClient\"/>.\n    /// <para>Before adding services, it internally configures options by registering the action in DI; thus an <see cref=\"IOptions{T}\"/> (where TOptions is <see cref=\"KubeClientOptions\"/>) object becomes available in the DI container.</para>\n    /// </summary>\n    /// <remarks>It operates in 2 modes:\n    /// <list type=\"number\">\n    ///   <item>If <paramref name=\"configureOptions\"/> is provided (action is not null), it calls the action ignoring all optional arguments.</item>\n    ///   <item>If <paramref name=\"configureOptions\"/> is not provided (action is null), it reads the global <see cref=\"FileGlobalConfiguration.ServiceDiscoveryProvider\"/> options and reuses them to initialize the following properties: <see cref=\"KubeClientOptions.ApiEndPoint\"/>, <see cref=\"KubeClientOptions.AccessToken\"/>, and <see cref=\"KubeClientOptions.KubeNamespace\"/>, finally initializing the rest of the properties with optional arguments.</item>\n    /// </list>\n    /// </remarks>\n    /// <param name=\"builder\">The Ocelot Builder instance.</param>\n    /// <param name=\"configureOptions\">An action to initialize <see cref=\"KubeClientOptions\"/> of the client. It can be null: read the remarks.</param>\n    /// <param name=\"defaultScheme\">Optional scheme to build <see cref=\"KubeClientOptions.ApiEndPoint\"/> URI when the global <see cref=\"FileServiceDiscoveryProvider.Scheme\"/> is unknown, defaulting to 'https' aka <see cref=\"Uri.UriSchemeHttps\"/>.</param>\n    /// <param name=\"defaultHost\">Optional host to build <see cref=\"KubeClientOptions.ApiEndPoint\"/> URI when the global <see cref=\"FileServiceDiscoveryProvider.Host\"/> is unknown, defaulting to 'localhost' aka <see cref=\"IPAddress.Loopback\"/>.</param>\n    /// <param name=\"defaultPort\">Optional port to build <see cref=\"KubeClientOptions.ApiEndPoint\"/> URI when the global <see cref=\"FileServiceDiscoveryProvider.Port\"/> is unknown, defaulting to 443.</param>\n    /// <param name=\"defaultNamespace\">Optional namespace to initialize <see cref=\"KubeClientOptions.KubeNamespace\"/> option when the global <see cref=\"FileServiceDiscoveryProvider.Namespace\"/> is unknown, defaulting to 'default'.</param>\n    /// <param name=\"username\">Optional username to initialize the <see cref=\"KubeClientOptions.Username\"/> option.</param>\n    /// <param name=\"password\">Optional password to initialize the <see cref=\"KubeClientOptions.Password\"/> option.</param>\n    /// <param name=\"accessTokenCommand\">Optional command to initialize the <see cref=\"KubeClientOptions.AccessTokenCommand\"/> option.</param>\n    /// <param name=\"accessTokenCommandArguments\">Optional arguments to initialize the <see cref=\"KubeClientOptions.AccessTokenCommandArguments\"/> option.</param>\n    /// <param name=\"accessTokenSelector\">Optional selector to initialize the <see cref=\"KubeClientOptions.AccessTokenSelector\"/> option.</param>\n    /// <param name=\"accessTokenExpirySelector\">Optional selector to initialize the <see cref=\"KubeClientOptions.AccessTokenExpirySelector\"/> option.</param>\n    /// <param name=\"initialAccessToken\">Optional token to initialize the <see cref=\"KubeClientOptions.InitialAccessToken\"/> option.</param>\n    /// <param name=\"initialTokenExpiryUtc\">Optional date-time to initialize the <see cref=\"KubeClientOptions.InitialTokenExpiryUtc\"/> option.</param>\n    /// <param name=\"clientCertificate\">Optional certificate to initialize the <see cref=\"KubeClientOptions.ClientCertificate\"/> option.</param>\n    /// <param name=\"certificationAuthorityCertificate\">Optional certificate to initialize the <see cref=\"KubeClientOptions.CertificationAuthorityCertificate\"/> option.</param>\n    /// <param name=\"allowInsecure\">Optional verification flag to initialize the <see cref=\"KubeClientOptions.AllowInsecure\"/> option, defaulting to false.</param>\n    /// <param name=\"authStrategy\">Optional strategy to initialize the <see cref=\"KubeClientOptions.AuthStrategy\"/> option, defaulting to <see cref=\"KubeAuthStrategy.BearerToken\"/>.</param>\n    /// <param name=\"logHeaders\">Optional log flag to initialize the <see cref=\"KubeClientOptions.LogHeaders\"/> option, defaulting to false.</param>\n    /// <param name=\"logPayloads\">Optional log flag to initialize the <see cref=\"KubeClientOptions.LogPayloads\"/> option, defaulting to false.</param>\n    /// <param name=\"loggerFactory\">Optional factory to initialize the <see cref=\"KubeClientOptions.LoggerFactory\"/> option.</param>\n    /// <param name=\"modelTypeAssemblies\">Optional list to add assemblies to the <see cref=\"KubeClientOptions.ModelTypeAssemblies\"/> option, defaulting to empty list.</param>\n    /// <param name=\"environmentVariables\">Optional dictionary to initialize the <see cref=\"KubeClientOptions.EnvironmentVariables\"/> option, defaulting to empty list.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, Action<KubeClientOptions> configureOptions, // required params\n        string defaultScheme = null, string defaultHost = null, int? defaultPort = null, string defaultNamespace = null, // optional params \n        string username = null, string password = null,\n        string accessTokenCommand = null, string accessTokenCommandArguments = null, string accessTokenSelector = null, string accessTokenExpirySelector = null,\n        string initialAccessToken = null, DateTime? initialTokenExpiryUtc = null,\n        X509Certificate2 clientCertificate = null, X509Certificate2 certificationAuthorityCertificate = null,\n        bool? allowInsecure = null, KubeAuthStrategy? authStrategy = null,\n        bool? logHeaders = null, bool? logPayloads = null, ILoggerFactory loggerFactory = null,\n        List<Assembly> modelTypeAssemblies = null, Dictionary<string, string> environmentVariables = null)\n    {\n        configureOptions ??= Configure;\n        builder.Services.AddOptions<KubeClientOptions>().Configure(configureOptions);\n        return builder.AddKubernetes(false);\n\n        void Configure(KubeClientOptions options)\n        {\n            // Initialize properties with values coming from global ServiceDiscoveryProvider options\n            var key = $\"{nameof(FileConfiguration.GlobalConfiguration)}:{nameof(FileGlobalConfiguration.ServiceDiscoveryProvider)}\";\n            var section = builder.Configuration.GetSection(key);\n            var scheme = section.Str(nameof(FileServiceDiscoveryProvider.Scheme), defaultScheme ?? Uri.UriSchemeHttps);\n            var host = section.Str(nameof(FileServiceDiscoveryProvider.Host), defaultHost ?? IPAddress.Loopback.ToString());\n            var port = section.Int(nameof(FileServiceDiscoveryProvider.Port), defaultPort ?? 443);\n            options.ApiEndPoint = new UriBuilder(scheme, host, port).Uri;\n            options.KubeNamespace = section.Str(nameof(FileServiceDiscoveryProvider.Namespace), defaultNamespace ?? \"default\");\n            options.AccessToken = section.GetValue<string>(nameof(FileServiceDiscoveryProvider.Token));\n\n            // Initialize properties with values coming from optional arguments\n            options.AuthStrategy = authStrategy ?? KubeAuthStrategy.BearerToken;\n            options.AllowInsecure = allowInsecure ?? false;\n            options.AccessTokenCommand = accessTokenCommand;\n            options.AccessTokenCommandArguments = accessTokenCommandArguments;\n            options.AccessTokenExpirySelector = accessTokenExpirySelector;\n            options.AccessTokenSelector = accessTokenSelector;\n            options.CertificationAuthorityCertificate = certificationAuthorityCertificate;\n            options.ClientCertificate = clientCertificate;\n            options.EnvironmentVariables = environmentVariables ?? new();\n            options.InitialAccessToken = initialAccessToken;\n            options.InitialTokenExpiryUtc = initialTokenExpiryUtc;\n            options.LoggerFactory = loggerFactory;\n            options.LogHeaders = logHeaders ?? false;\n            options.LogPayloads = logPayloads ?? false;\n            options.ModelTypeAssemblies.AddRange(modelTypeAssemblies ?? new());\n            options.Password = password;\n            options.Username = username;\n        }\n    }\n\n    private static string Str(this IConfigurationSection sec, string key, string defaultValue)\n    {\n        string val = sec.GetValue<string>(key, defaultValue);\n        return string.IsNullOrEmpty(val) ? defaultValue : val;\n    }\n\n    private static int Int(this IConfigurationSection sec, string key, int defaultValue)\n    {\n        int val = sec.GetValue<int>(key, defaultValue);\n        return val <= 0 ? defaultValue : val;\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/PollKube.cs",
    "content": "﻿using Ocelot.Logging;\nusing Ocelot.Values;\nusing System.Collections.Concurrent;\nusing YamlDotNet.Core.Tokens;\n\nnamespace Ocelot.Provider.Kubernetes;\n\n/// <summary>\n/// It polls the <see cref=\"Kube\"/> provider in the specified intervals and update the queue with new versions of services.\n/// </summary>\npublic class PollKube : IServiceDiscoveryProvider, IDisposable\n{\n    private readonly IOcelotLogger _logger;\n    private readonly IServiceDiscoveryProvider _provider; // TODO IDisposable\n    private readonly ConcurrentQueue<List<Service>> _queue = new();\n    public static readonly List<Service> Empty = new(0);\n\n    private Task _timing;\n    private PeriodicTimer _timer;\n    private CancellationTokenSource _cts = new();\n    private volatile bool _polling, _disposed, _stopped;\n\n    public PollKube(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeProvider)\n    {\n        _logger = factory.CreateLogger<PollKube>();\n        _provider = kubeProvider;\n        _timer = new(TimeSpan.FromMilliseconds(pollingInterval));\n    }\n\n    public async Task<List<Service>> GetAsync()\n    {\n        _timing ??= StartAsync(); // (_cts.Token);\n        if (_disposed || _cts.IsCancellationRequested)\n            return Empty;\n\n        // First cold request must call the provider\n        if (_queue.IsEmpty)\n        {\n            return await PollAsync(_cts.Token);\n        }\n        else if (_polling && _queue.TryPeek(out var oldVersion))\n        {\n            return oldVersion;\n        }\n\n        // For services with multiple versions, remove outdated versions and retain only the latest one\n        while (!_polling && _queue.Count > 1 && _queue.TryDequeue(out _))\n        {\n        }\n\n        _queue.TryPeek(out var latestVersion);\n        return latestVersion;\n    }\n\n    protected virtual async Task<List<Service>> PollAsync(CancellationToken token)\n    {\n        if (_disposed || token.IsCancellationRequested)\n            return Empty;\n\n        // Avoid polling if already in progress due to a slow completion of the PollAsync task,\n        // and ensure no more than three versions of services remain in the queue.\n        if (_polling || _queue.Count > 3)\n            return Empty; // but don't enqueue\n\n        try\n        {\n            _polling = true;\n            var services = await _provider.GetAsync(); // TODO Add cancellation\n            if (_disposed || token.IsCancellationRequested)\n                return Empty;\n\n            _queue.Enqueue(services);\n            return services;\n        }\n        catch (ObjectDisposedException)\n        {\n            return Empty;\n        }\n        finally\n        {\n            _polling = false;\n        }\n    }\n\n    /// <summary>\n    /// Endless task which should be stopped during disposing or when the provider is no longer needed.\n    /// </summary>\n    protected async Task StartAsync()\n    {\n        try\n        {\n            while (!_disposed && !_stopped &&\n                await _timer.WaitForNextTickAsync(_cts.Token))\n            {\n                await PollAsync(_cts.Token);\n            }\n        }\n        catch (OperationCanceledException)\n        {\n            // Expected on cancellation aka _cts.Cancel()\n        }\n        finally\n        {\n            _queue.Clear();\n        }\n    }\n\n    protected void Stop()\n    {\n        if (_disposed)\n            return;\n\n        _cts.Cancel();\n        _timer?.Dispose();\n        _stopped = true; // the flag ensures the loop will exit\n        _timing?.GetAwaiter().GetResult(); // due to the flag this wait should complete in a reasonable time, in polling interval at most\n        _timing?.Dispose();\n        _cts.Dispose();\n    }\n\n    #region Dispose pattern\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n            return;\n\n        if (disposing)\n        {\n            Stop();\n            _logger?.Dispose();\n        }\n\n        //_cts = null;        \n        _timer = null;\n        _timing = null;\n        _disposed = true;\n    }\n\n    ~PollKube() => Dispose(false);\n    #endregion\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using KubeClient;\nglobal using Ocelot;\nglobal using Ocelot.ServiceDiscovery;\nglobal using Ocelot.ServiceDiscovery.Providers;\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/WatchKube.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\nusing System.Reactive.Concurrency;\nusing System.Reactive.Linq;\n\nnamespace Ocelot.Provider.Kubernetes;\n\npublic class WatchKube : IServiceDiscoveryProvider, IDisposable\n{\n    /// <summary>The default number of seconds to wait before scheduling the next retry for the subscription operation.</summary>\n    /// <value>A positive integer that is greater than or equal to 1.</value>\n    public static int FailedSubscriptionRetrySeconds\n    {\n        get => failedSubscriptionRetrySeconds;\n        set => failedSubscriptionRetrySeconds = value >= 1 ? value : 1;\n    }\n\n    /// <summary>The default number of seconds to wait after Ocelot starts, following the provider's creation, to fetch the first result from the Kubernetes endpoint.</summary>\n    /// <value>A positive integer that is greater than or equal to 1.</value>\n    public static int FirstResultsFetchingTimeoutSeconds\n    {\n        get => firstResultsFetchingTimeoutSeconds;\n        set => firstResultsFetchingTimeoutSeconds = value >= 1 ? value : 1;\n    }\n\n    private static int failedSubscriptionRetrySeconds = 1;\n    private static int firstResultsFetchingTimeoutSeconds = 1;\n\n    private readonly KubeRegistryConfiguration _configuration;\n    private readonly IOcelotLogger _logger;\n    private readonly IKubeApiClient _kubeApi;\n    private readonly IKubeServiceBuilder _serviceBuilder;\n    private readonly IScheduler _scheduler;\n    \n    private readonly IDisposable _subscription;\n    private TaskCompletionSource _firstResultsCompletionSource;\n    \n    private List<Service> _services = new();\n\n    public WatchKube(\n        KubeRegistryConfiguration configuration,\n        IOcelotLoggerFactory factory,\n        IKubeApiClient kubeApi,\n        IKubeServiceBuilder serviceBuilder,\n        IScheduler scheduler)\n    {\n        _configuration = configuration;\n        _logger = factory.CreateLogger<WatchKube>();\n        _kubeApi = kubeApi;\n        _serviceBuilder = serviceBuilder;\n        _scheduler = scheduler;\n\n        SetFirstResultsCompletedAfterDelay();\n        _subscription = CreateSubscription();\n    }\n\n    public virtual async Task<List<Service>> GetAsync()\n    {\n        // Wait for first results fetching\n        await _firstResultsCompletionSource.Task;\n        if (_services.Count == 0)\n        {\n            _logger.LogWarning(() => GetMessage(\"Subscription to service endpoints gave no results!\"));\n        }\n\n        return _services;\n    }\n\n    private void SetFirstResultsCompletedAfterDelay()\n    {\n        _firstResultsCompletionSource = new();\n        Observable\n            .Timer(TimeSpan.FromSeconds(FirstResultsFetchingTimeoutSeconds), _scheduler)\n            .Subscribe(_ => _firstResultsCompletionSource.TrySetResult());\n    }\n\n    private void OnNext(IResourceEventV1<EndpointsV1> endpointEvent)\n    {\n        _services = endpointEvent.EventType switch\n        {\n            ResourceEventType.Deleted or ResourceEventType.Error => new(),\n            _ when (endpointEvent.Resource?.Subsets.Count ?? 0) == 0 => new(),\n            _ => _serviceBuilder.BuildServices(_configuration, endpointEvent.Resource).ToList(),\n        };\n        _firstResultsCompletionSource.TrySetResult();\n    }\n\n    // Called only when subscription canceled in Dispose\n    private void OnCompleted() => _logger.LogInformation(() => GetMessage(\"Subscription to service endpoints completed\"));\n    private void OnException(Exception ex) => _logger.LogError(() => GetMessage(\"Endpoints subscription error occured.\"), ex);\n\n    private IDisposable CreateSubscription() => _kubeApi\n            .EndpointsV1()\n            .Watch(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace)\n            .Do(_ => { }, OnException)\n            .RetryAfter(TimeSpan.FromSeconds(FailedSubscriptionRetrySeconds), _scheduler)\n            .Subscribe(OnNext, OnCompleted);\n\n    private string GetMessage(string message)\n        => $\"{nameof(WatchKube)} provider. Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; {message}\";\n\n    public void Dispose()\n    {\n        _subscription.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Kubernetes/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"KubeClient\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"tL9cSl3maS5FPzp/3MtlZI21ExWhni0nnUCF8HY4npTsINw45n9SNDbkKXBMtFyUFGSsQep25fHIDN4f/Vp3AQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {},\n    \"net10.0/win-x64\": {},\n    \"net8.0\": {\n      \"KubeClient\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"8.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.2\",\n        \"contentHash\": \"3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {},\n    \"net8.0/win-x64\": {},\n    \"net9.0\": {\n      \"KubeClient\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.1, )\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"9.0.3\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"RIEeZxWYm77+OWLwgik7DzSVSONjqkmcbuCb1koZdGAV7BgOUWnLz80VMyHZMw3onrVwFCCMHBBdruBPuQTvkg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Primitives\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"q5qlbm6GRUrle2ZZxy9aqS/wWoc+mRD3JeP6rcpiJTh5XcemYkplAcJKq8lU11ZfPom5lfbZZfnQvDqcUhqD5Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"ad82pYBUSQbd3WIboxsS1HzFdRuHKRa2CpYwie/o6dZAxUjt62yFwjoVdM7Iw2VO5fHV1rJwa7jJZBNZin0E7Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"lDbxJpkl6X8KZGpkAxgrrthQ42YeiR0xjPp7KPx+sCPc3ZbpaIbjzd0QQ+9kDdK2RU2DOl3pc6tQyAgEZY3V0A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"utIi2R1nm+PCWkvWBf1Ou6LWqg9iLfHU23r8yyU9VCvda4dEs7xbTZSwGa5KuwbpzpgCbHCIuKaFHB3zyFmnGw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"H/MBMLt9A/69Ux4OrV7oCKt3DcMT04o5SCqDolulzQA66TLFEpYYb4qedMs/uwrLtyHXGuDGWKZse/oa8W9AZw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"xE7MpY70lkw1oiid5y6FbL9dVw8oLfkx8RhSNGN8sSzBlCqGn0SyT3Fqc8tZnDaPIq7Z8R9RTKlS564DS+MV3g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Primitives\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"PcyYHQglKnWVZHSPaL6v2qnfsIuFw8tSq7cyXHg3OeuDVn/CqmdWUjRiZomCF/Gi+qCi+ksz0lFphg2cNvB8zQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\",\n          \"Microsoft.Extensions.Primitives\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"yCCJHvBcRyqapMSNzP+kTc57Eaavq2cr5Tmuil6/XVnipQf5xmskxakSQ1enU6S4+fNg3sJ27WcInV64q24JsA==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {},\n    \"net9.0/win-x64\": {}\n  }\n}"
  },
  {
    "path": "src/Ocelot.Provider.Polly/CircuitBreakerStrategy.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.Provider.Polly;\n\n/// <summary>\n/// Polly requirements for the <see href=\"https://www.pollydocs.org/strategies/circuit-breaker.html\">Circuit breaker resilience strategy</see>.\n/// The subjects of this strategy are the <see cref=\"QoSOptions.MinimumThroughput\"/> and <see cref=\"QoSOptions.BreakDuration\"/> properties.\n/// </summary>\npublic static class CircuitBreakerStrategy\n{\n    // --- BreakDuration ---\n    // Actual Polly's BreakDuration constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration\n    public const int LowBreakDuration = 500; // 0.5 seconds\n    public const int HighBreakDuration = 86_400_000; // 1 day, 24 hours in milliseconds\n\n    /// <summary>Default duration of break the circuit will stay open before resetting, in milliseconds.</summary>\n    public const int DefaultBreakDuration = 5_000; // 5 seconds\n\n    /// <summary>\n    /// Applies Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration\">BreakDuration</see> constraint to the value.\n    /// <para>If using Polly v8 or later, and in accordance with Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration\">BreakDuration</see> constraint, this value must be greater than <see cref=\"LowBreakDuration\"/> (0.5 seconds) and less than <see cref=\"HighBreakDuration\"/> (1 day).</para></summary>\n    /// <param name=\"milliseconds\">The value in milliseconds.</param>\n    /// <returns>The same value if the constraint is satisfied; otherwise, the default value (<see cref=\"DefaultBreakDuration\"/>).</returns>\n    public static int BreakDuration(int milliseconds) => IsValidBreakDuration(milliseconds) ? milliseconds : DefaultBreakDuration;\n    public static bool IsValidBreakDuration(this int milliseconds) => milliseconds > LowBreakDuration && milliseconds < HighBreakDuration;\n\n    // --- MinimumThroughput ---\n    // Actual Polly's MinimumThroughput constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput\n    public const int LowMinimumThroughput = 2;\n\n    /// <summary>Default minimum throughput: this many actions or more must pass through the circuit in the time-slice, for statistics to be considered significant and the circuit-breaker to come into action.</summary>\n    public const int DefaultMinimumThroughput = 100;\n\n    /// <summary>\n    /// Applies Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput\">MinimumThroughput</see> constraint to the value.\n    /// <para>If using Polly v8 or later, and in accordance with Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput\">MinimumThroughput</see> constraint, this value must be <see cref=\"LowMinimumThroughput\"/> (2) or greater.</para></summary>\n    /// <param name=\"failures\">The number of failures.</param>\n    /// <returns>The same value if the constraint is satisfied; otherwise, the default value (<see cref=\"DefaultMinimumThroughput\"/>).</returns>\n    public static int MinimumThroughput(int failures) => IsValidMinimumThroughput(failures) ? failures : DefaultMinimumThroughput;\n    public static bool IsValidMinimumThroughput(this int failures) => failures >= LowMinimumThroughput;\n\n    // --- FailureRatio ---\n    // Actual Polly's FailureRatio constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio\n    public const double LowFailureRatio = 0.0D; // ~ 0%\n    public const double HighFailureRatio = 1.0D; // ~100%\n\n    /// <summary>The FailureRatio default value is 0.1 (i.e. 10%).</summary>\n    public const double DefaultFailureRatio = 0.1D; // ~10%\n\n    /// <summary>\n    /// Applies Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio\">FailureRatio</see> constraint to the value.\n    /// <para>If using Polly v8 or later, and in accordance with Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio\">FailureRatio</see> constraint, this value must be greater than <see cref=\"LowFailureRatio\"/> (0) and less than <see cref=\"HighFailureRatio\"/> (1).</para></summary>\n    /// <param name=\"ratio\">The value as quotient (~ percents).</param>\n    /// <returns>The same value if the constraint is satisfied; otherwise, the default value (<see cref=\"DefaultFailureRatio\"/>).</returns>\n    public static double FailureRatio(double ratio) => IsValidFailureRatio(ratio) ? ratio : DefaultFailureRatio;\n    public static bool IsValidFailureRatio(this double ratio) => ratio > LowFailureRatio && ratio < HighFailureRatio;\n\n    // --- SamplingDuration ---\n    // Actual Polly's SamplingDuration constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration\n    public const int LowSamplingDuration = 500; // 0.5 seconds\n    public const int HighSamplingDuration = 86_400_000; // 1 day, 24 hours in milliseconds\n\n    /// <summary>The SamplingDuration default value is 30 seconds, in milliseconds.</summary>\n    public const int DefaultSamplingDuration = 30_000; // 30 seconds\n\n    /// <summary>\n    /// Applies Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration\">SamplingDuration</see> constraint to the value.\n    /// <para>If using Polly v8 or later, and in accordance with Polly's <see href=\"https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration\">SamplingDuration</see> constraint, this value must be greater than <see cref=\"LowSamplingDuration\"/> (0.5 seconds) and less than <see cref=\"HighSamplingDuration\"/> (1 day).</para></summary>\n    /// <param name=\"milliseconds\">The value in milliseconds.</param>\n    /// <returns>The same value if the constraint is satisfied; otherwise, the default value (<see cref=\"DefaultSamplingDuration\"/>).</returns>\n    public static int SamplingDuration(int milliseconds) => IsValidSamplingDuration(milliseconds) ? milliseconds : DefaultSamplingDuration;\n    public static bool IsValidSamplingDuration(this int milliseconds) => milliseconds > LowSamplingDuration && milliseconds < HighSamplingDuration;\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/Interfaces/IPollyQoSResiliencePipelineProvider.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.Provider.Polly.Interfaces;\n\n/// <summary>Defines provider for Polly V8 pipelines.</summary>\n/// <typeparam name=\"TResult\">An HTTP result type, usually it is <see cref=\"HttpResponseMessage\"/> type.</typeparam>\npublic interface IPollyQoSResiliencePipelineProvider<TResult>\n    where TResult : IDisposable\n{\n    /// <summary>\n    /// Gets Polly v8 pipeline.\n    /// </summary>\n    /// <param name=\"route\">The route to apply a pipeline for.</param>\n    /// <returns>A <see cref=\"ResiliencePipeline{T}\"/> object where T is <typeparamref name=\"TResult\"/>.</returns>\n    ResiliencePipeline<TResult> GetResiliencePipeline(DownstreamRoute route);\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <NoPackageAnalysis>true</NoPackageAnalysis>\n    <Description>Provides Ocelot extensions to use Polly.NET</Description>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <AssemblyName>Ocelot.Provider.Polly</AssemblyName>\n    <PackageId>Ocelot.Provider.Polly</PackageId>\n    <PackageTags>API Gateway;.NET;Polly</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Polly</PackageProjectUrl>\n    <PackageIconUrl>https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png</PackageIconUrl>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <Authors>Tom Pallister, Raman Maksimchuk, Raynald Messié</Authors>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <NoWarn>1591</NoWarn>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <DebugType>full</DebugType>\n    <DebugSymbols>True</DebugSymbols>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"..\\..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ocelot\\Ocelot.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Polly\" Version=\"8.6.6\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Errors;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly.Interfaces;\nusing Ocelot.QualityOfService;\nusing Polly.CircuitBreaker;\nusing Polly.Registry;\nusing Polly.Timeout;\n\nnamespace Ocelot.Provider.Polly;\n\npublic static class OcelotBuilderExtensions\n{\n    /// <summary>\n    /// Default mapping of Polly <see cref=\"Exception\"/>s to <see cref=\"Error\"/> objects.\n    /// </summary>\n    public static readonly IDictionary<Type, Func<Exception, Error>> DefaultErrorMapping = new Dictionary<Type, Func<Exception, Error>>\n    {\n        {typeof(TaskCanceledException), CreateRequestTimedOutError},\n        {typeof(TimeoutRejectedException), CreateRequestTimedOutError},\n        {typeof(BrokenCircuitException), CreateRequestTimedOutError},\n        {typeof(BrokenCircuitException<HttpResponseMessage>), CreateRequestTimedOutError},\n    };\n\n    private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e);\n\n    /// <summary>\n    /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping.\n    /// </summary>\n    /// <typeparam name=\"TProvider\">QoS provider to use (by default use <see cref=\"PollyQoSResiliencePipelineProvider\"/>).</typeparam>\n    /// <param name=\"builder\">Ocelot builder to extend.</param>\n    /// <param name=\"delegatingHandler\">Your customized delegating handler (to manage QoS behavior by yourself).</param>\n    /// <param name=\"errorMapping\">Your customized error mapping.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddPolly<TProvider>(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary<Type, Func<Exception, Error>> errorMapping)\n        where TProvider : class, IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\n    {\n        builder.Services\n            .AddSingleton<ResiliencePipelineRegistry<OcelotResiliencePipelineKey>>()\n            .AddSingleton(errorMapping) // Dictionary<TKey, TValue> injection used in HttpExceptionToErrorMapper\n            .AddSingleton<IPollyQoSResiliencePipelineProvider<HttpResponseMessage>, TProvider>()\n            .AddSingleton(delegatingHandler);\n        return builder;\n    }\n\n    /// <summary>\n    /// Adds Polly QoS provider to Ocelot with custom error mapping, but default <see cref=\"DelegatingHandler\"/> is used.\n    /// </summary>\n    /// <typeparam name=\"TProvider\">QoS provider to use (by default use <see cref=\"PollyQoSResiliencePipelineProvider\"/>).</typeparam>\n    /// <param name=\"builder\">Ocelot builder to extend.</param>\n    /// <param name=\"errorMapping\">Your customized error mapping.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddPolly<TProvider>(this IOcelotBuilder builder, IDictionary<Type, Func<Exception, Error>> errorMapping)\n        where TProvider : class, IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\n        => AddPolly<TProvider>(builder, GetDelegatingHandler, errorMapping);\n\n    /// <summary>\n    /// Adds Polly QoS provider to Ocelot with custom <see cref=\"DelegatingHandler\"/> delegate, but default error mapping is used.\n    /// </summary>\n    /// <typeparam name=\"TProvider\">QoS provider to use (by default use <see cref=\"PollyQoSResiliencePipelineProvider\"/>).</typeparam>\n    /// <param name=\"builder\">Ocelot builder to extend.</param>\n    /// <param name=\"delegatingHandler\">Your customized delegating handler (to manage QoS behavior by yourself).</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddPolly<TProvider>(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler)\n        where TProvider : class, IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\n        => AddPolly<TProvider>(builder, delegatingHandler, DefaultErrorMapping);\n\n    /// <summary>\n    /// Adds Polly QoS provider to Ocelot by defaults.\n    /// </summary>\n    /// <remarks>\n    /// Defaults:\n    /// <list type=\"bullet\">\n    ///   <item><see cref=\"GetDelegatingHandler\"/></item>\n    ///   <item><see cref=\"DefaultErrorMapping\"/></item>\n    /// </list>\n    /// </remarks>\n    /// <typeparam name=\"TProvider\">QoS provider to use (by default use <see cref=\"PollyQoSResiliencePipelineProvider\"/>).</typeparam>\n    /// <param name=\"builder\">Ocelot builder to extend.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddPolly<TProvider>(this IOcelotBuilder builder)\n        where TProvider : class, IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\n        => AddPolly<TProvider>(builder, GetDelegatingHandler, DefaultErrorMapping);\n\n    /// <summary>\n    /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider.\n    /// </summary>\n    /// <remarks>\n    /// Defaults:\n    /// <list type=\"bullet\">\n    ///   <item><see cref=\"PollyQoSResiliencePipelineProvider\"/></item>\n    ///   <item><see cref=\"GetDelegatingHandler\"/></item>\n    ///   <item><see cref=\"DefaultErrorMapping\"/></item>\n    /// </list>\n    /// </remarks>\n    /// <param name=\"builder\">Ocelot builder to extend.</param>\n    /// <returns>The reference to the same extended <see cref=\"IOcelotBuilder\"/> object.</returns>\n    public static IOcelotBuilder AddPolly(this IOcelotBuilder builder)\n        => AddPolly<PollyQoSResiliencePipelineProvider>(builder, GetDelegatingHandler, DefaultErrorMapping);\n\n    /// <summary>\n    /// Creates default delegating handler based on the <see cref=\"PollyResiliencePipelineDelegatingHandler\"/> type.\n    /// </summary>\n    /// <param name=\"route\">The downstream route to apply the handler for.</param>\n    /// <param name=\"contextAccessor\">The context accessor of the route.</param>\n    /// <param name=\"loggerFactory\">The factory of logger.</param>\n    /// <returns>A <see cref=\"DelegatingHandler\"/> object, but concrete type is the <see cref=\"PollyResiliencePipelineDelegatingHandler\"/> class.</returns>\n    private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory)\n        => new PollyResiliencePipelineDelegatingHandler(route, contextAccessor, loggerFactory);\n}\r\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/OcelotResiliencePipelineKey.cs",
    "content": "using Polly.Registry;\n\nnamespace Ocelot.Provider.Polly;\n\n/// <summary>\n/// Object used to identify a resilience pipeline in <see cref=\"ResiliencePipelineRegistry{OcelotResiliencePipelineKey}\"/>.\n/// </summary>\n/// <param name=\"Key\">The key for the resilience pipeline.</param>\npublic record OcelotResiliencePipelineKey(string Key);\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly.Interfaces;\nusing Polly.CircuitBreaker;\nusing Polly.Registry;\nusing Polly.Timeout;\nusing System.Linq;\nusing System.Net;\n\nnamespace Ocelot.Provider.Polly;\n\n/// <summary>\n/// Default provider for Polly V8 pipelines.\n/// </summary>\npublic class PollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\n{\n    private readonly ResiliencePipelineRegistry<OcelotResiliencePipelineKey> _registry;\n    private readonly IOcelotLogger _logger;\n    \n    public PollyQoSResiliencePipelineProvider(\n        IOcelotLoggerFactory loggerFactory,\n        ResiliencePipelineRegistry<OcelotResiliencePipelineKey> registry)\n    {\n        _logger = loggerFactory?.CreateLogger<PollyQoSResiliencePipelineProvider>() ?? throw new ArgumentNullException(nameof(loggerFactory));\n        _registry = registry ?? throw new ArgumentNullException(nameof(registry));\n    }\n\n    public static readonly IReadOnlySet<HttpStatusCode> DefaultServerErrorCodes = new HashSet<HttpStatusCode>()\n    {\n        HttpStatusCode.InternalServerError,\n        HttpStatusCode.NotImplemented,\n        HttpStatusCode.BadGateway,\n        HttpStatusCode.ServiceUnavailable,\n        HttpStatusCode.GatewayTimeout,\n        HttpStatusCode.HttpVersionNotSupported,\n        HttpStatusCode.VariantAlsoNegotiates,\n        HttpStatusCode.InsufficientStorage,\n        HttpStatusCode.LoopDetected,\n    };\n\n    protected virtual HashSet<HttpStatusCode> ServerErrorCodes { get; } = DefaultServerErrorCodes as HashSet<HttpStatusCode>;\n    protected virtual string GetRouteName(DownstreamRoute route) => route.Name();\n\n    /// <summary>\n    /// Gets Polly V8 resilience pipeline (applies QoS feature) for the route.\n    /// </summary>\n    /// <param name=\"route\">The downstream route to apply the pipeline for.</param>\n    /// <returns>A <see cref=\"ResiliencePipeline{T}\"/> object where T is <see cref=\"HttpResponseMessage\"/>.</returns>\n    public ResiliencePipeline<HttpResponseMessage> GetResiliencePipeline(DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n\n        if (!route.QosOptions.UseQos)\n        {\n            return ResiliencePipeline<HttpResponseMessage>.Empty; // shortcut -> No QoS\n        }\n\n        return _registry.GetOrAddPipeline<HttpResponseMessage>(\n            key: new OcelotResiliencePipelineKey(route.LoadBalancerKey),\n            configure: (builder) => ConfigureStrategies(builder, route));\n    }\n\n    protected virtual void ConfigureStrategies(ResiliencePipelineBuilder<HttpResponseMessage> builder, DownstreamRoute route)\n    {\n        ConfigureCircuitBreaker(builder, route);\n        ConfigureTimeout(builder, route);\n    }\n\n    protected virtual string CircuitBreakerValidationMessage(DownstreamRoute route)\n        => $\"Route '{GetRouteName(route)}' has invalid {nameof(QoSOptions)} for Polly's Circuit Breaker strategy. Specifically, \";\n\n    protected virtual bool IsConfigurationValidForCircuitBreaker(DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(route.QosOptions);\n\n        var qos = route.QosOptions;\n        if (!qos.MinimumThroughput.HasValue || qos.MinimumThroughput <= 0)\n        {\n            _logger.LogError(\n                () => CircuitBreakerValidationMessage(route) + $\"the circuit breaker is disabled because the {nameof(qos.MinimumThroughput)} value ({ToStr(qos.MinimumThroughput)}) is either undefined, negative, or zero.\", null);\n            return false;\n        }\n\n        List<Func<string>> warnings = new(), w = warnings;\n        if (!qos.MinimumThroughput.Value.IsValidMinimumThroughput())\n        {\n            string msg1() => $\"{The(w, msg1)} {nameof(CircuitBreakerStrategy.MinimumThroughput)} value ({qos.MinimumThroughput}) is less than the required {nameof(CircuitBreakerStrategy.LowMinimumThroughput)} threshold ({CircuitBreakerStrategy.LowMinimumThroughput}). Therefore, increase {nameof(qos.MinimumThroughput)} to at least {CircuitBreakerStrategy.LowMinimumThroughput} or higher. Until then, the default value ({CircuitBreakerStrategy.DefaultMinimumThroughput}) will be substituted.\";\n            warnings.Add(msg1);\n        }\n\n        if (qos.BreakDuration.HasValue && !qos.BreakDuration.Value.IsValidBreakDuration())\n        {\n            string msg2() => $\"{The(w, msg2)} {nameof(CircuitBreakerStrategy.BreakDuration)} value ({qos.BreakDuration}) is outside the valid range ({CircuitBreakerStrategy.LowBreakDuration} to {CircuitBreakerStrategy.HighBreakDuration} milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultBreakDuration}) will be substituted.\";\n            warnings.Add(msg2);\n        }\n\n        if (qos.FailureRatio.HasValue && !qos.FailureRatio.Value.IsValidFailureRatio())\n        {\n            string msg3() => $\"{The(w, msg3)} {nameof(CircuitBreakerStrategy.FailureRatio)} value ({qos.FailureRatio}) is outside the valid range ({CircuitBreakerStrategy.LowFailureRatio} to {CircuitBreakerStrategy.HighFailureRatio}). Therefore, ensure the ratio falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultFailureRatio}) will be substituted.\";\n            warnings.Add(msg3);\n        }\n\n        if (qos.SamplingDuration.HasValue && !qos.SamplingDuration.Value.IsValidSamplingDuration())\n        {\n            string msg4() => $\"{The(w, msg4)} {nameof(CircuitBreakerStrategy.SamplingDuration)} value ({qos.SamplingDuration}) is outside the valid range ({CircuitBreakerStrategy.LowSamplingDuration} to {CircuitBreakerStrategy.HighSamplingDuration} milliseconds). Therefore, ensure the duration falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultSamplingDuration}) will be substituted.\";\n            warnings.Add(msg4);\n        }\n\n        if (warnings.Count > 0)\n        {\n            _logger.LogWarning(() => CircuitBreakerValidationMessage(route) + string.Join(string.Empty, warnings.Select(f => f.Invoke())));\n        }\n\n        return true;\n    }\n\n    protected virtual string TimeoutValidationMessage(DownstreamRoute route)\n        => $\"Route '{GetRouteName(route)}' has invalid {nameof(QoSOptions)} for Polly's Timeout strategy. Specifically, \";\n\n    protected virtual bool IsConfigurationValidForTimeout(DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(route.QosOptions);\n\n        int? timeoutMs = route.QosOptions.Timeout;\n        if (!timeoutMs.HasValue || timeoutMs.Value <= 0)\n        {\n            _logger.LogError(\n                () => TimeoutValidationMessage(route) + $\"the timeout is disabled because the {nameof(QoSOptions.Timeout)} ({ToStr(timeoutMs)}) is either undefined, negative, or zero.\", null);\n            return false;\n        }\n\n        List<Func<string>> warnings = new(), w = warnings;\n        if (!timeoutMs.Value.IsValidTimeout())\n        {\n            string msg() => $\"{The(w, msg)} {nameof(TimeoutStrategy.Timeout)} value ({timeoutMs.Value}) is outside the valid range ({TimeoutStrategy.LowTimeout} to {TimeoutStrategy.HighTimeout} milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value ({TimeoutStrategy.DefaultTimeout}) will be substituted.\";\n            warnings.Add(msg);\n        }\n\n        if (warnings.Count > 0)\n        {\n            _logger.LogWarning(() => TimeoutValidationMessage(route) + string.Join(string.Empty, warnings.Select(f => f.Invoke())));\n        }\n\n        return true;\n    }\n\n    public static string ToStr(int? value) => value.HasValue ? value.ToString() : \"?\";\n\n    public static string The(List<Func<string>> warnings, Func<string> msg)\n        => warnings.Count > 1\n            ? $\"{Environment.NewLine}  {warnings.IndexOf(msg) + 1}. The\"\n            : \"the\";\n\n    /// <summary>Configures the <see href=\"https://www.pollydocs.org/strategies/circuit-breaker.html\">Circuit breaker resilience strategy</see>.</summary>\n    /// <param name=\"builder\">Pipeline builder instance.</param>\n    /// <param name=\"route\">The route the pipeline is applied to.</param>\n    /// <returns>The same pipeline builder, as an <see cref=\"ResiliencePipelineBuilder{HttpResponseMessage}\"/> object where TResult is <see cref=\"HttpResponseMessage\"/>.</returns>\n    protected virtual ResiliencePipelineBuilder<HttpResponseMessage> ConfigureCircuitBreaker(ResiliencePipelineBuilder<HttpResponseMessage> builder, DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(route.QosOptions);\n        if (!IsConfigurationValidForCircuitBreaker(route))\n        {\n            return builder;\n        }\n\n        var info = $\"Circuit Breaker for the route: {GetRouteName(route)}: \";\n        QoSOptions qos = route.QosOptions;\n        int minimumThroughput = CircuitBreakerStrategy.MinimumThroughput(qos.MinimumThroughput ?? 0); // 0 fallbacks to the default value\n        int breakDurationMs = CircuitBreakerStrategy.BreakDuration(qos.BreakDuration ?? 0); // 0 fallbacks to the default value\n        double failureRatio = CircuitBreakerStrategy.FailureRatio(qos.FailureRatio ?? 0.0D); // 0 fallbacks to the default value\n        int samplingDurationMs = CircuitBreakerStrategy.SamplingDuration(qos.SamplingDuration ?? 0); // 0 fallbacks to the default value\n\n        var strategy = new CircuitBreakerStrategyOptions<HttpResponseMessage>\n        {\n            FailureRatio = failureRatio,\n            SamplingDuration = TimeSpan.FromMilliseconds(samplingDurationMs),\n            MinimumThroughput = minimumThroughput,\n            BreakDuration = TimeSpan.FromMilliseconds(breakDurationMs),\n            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()\n                .HandleResult(message => ServerErrorCodes.Contains(message.StatusCode))\n                .Handle<TimeoutRejectedException>()\n                .Handle<TimeoutException>(),\n            OnOpened = args =>\n            {\n                _logger.LogError(info + $\"Breaking for {args.BreakDuration.TotalMilliseconds} ms\",\n                    args.Outcome.Exception);\n                return ValueTask.CompletedTask;\n            },\n            OnClosed = _ =>\n            {\n                _logger.LogInformation(info + \"Closed\");\n                return ValueTask.CompletedTask;\n            },\n            OnHalfOpened = _ =>\n            {\n                // TODO: But the OnCircuitHalfOpened telemetry best practice recommends Warning 8-)\n                // Read -> Circuit breaker Telemetry -> https://www.pollydocs.org/strategies/circuit-breaker.html#telemetry\n                _logger.LogInformation(info + \"Half Opened\");\n                return ValueTask.CompletedTask;\n            },\n        };\n        return builder.AddCircuitBreaker(strategy);\n    }\n\n    /// <summary>Configures the <see href=\"https://www.pollydocs.org/strategies/timeout.html\">Timeout resilience strategy</see>.</summary>\n    /// <param name=\"builder\">Pipeline builder instance.</param>\n    /// <param name=\"route\">The route the pipeline is applied to.</param>\n    /// <returns>The same pipeline builder, as an <see cref=\"ResiliencePipelineBuilder{HttpResponseMessage}\"/> object where TResult is <see cref=\"HttpResponseMessage\"/>.</returns>\n    protected virtual ResiliencePipelineBuilder<HttpResponseMessage> ConfigureTimeout(ResiliencePipelineBuilder<HttpResponseMessage> builder, DownstreamRoute route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        ArgumentNullException.ThrowIfNull(route.QosOptions);\n\n        if (!IsConfigurationValidForTimeout(route))\n        {\n            return builder;\n        }\n\n        int? timeoutMs = route.QosOptions.Timeout ?? TimeoutStrategy.DefaultTimeout;\n        timeoutMs = TimeoutStrategy.Timeout(timeoutMs.Value) ?? TimeoutStrategy.DefaultTimeout;\n\n        var strategy = new TimeoutStrategyOptions\n        {\n            Timeout = TimeSpan.FromMilliseconds(timeoutMs.Value),\n            OnTimeout = _ =>\n            {\n                _logger.LogInformation(() => $\"Timeout for the route: {GetRouteName(route)}\");\n                return ValueTask.CompletedTask;\n            },\n        };\n        return builder.AddTimeout(strategy);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/PollyResiliencePipelineDelegatingHandler.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly.Interfaces;\nusing Polly.CircuitBreaker;\nusing System.Diagnostics;\n\nnamespace Ocelot.Provider.Polly;\n\npublic class PollyResiliencePipelineDelegatingHandler : DelegatingHandler\n{\n    private readonly DownstreamRoute _route;\n    private readonly IHttpContextAccessor _contextAccessor;\n    private readonly IOcelotLogger _logger;\n\n    public PollyResiliencePipelineDelegatingHandler(\n        DownstreamRoute route,\n        IHttpContextAccessor contextAccessor,\n        IOcelotLoggerFactory loggerFactory)\n    {\n        _route = route;\n        _contextAccessor = contextAccessor;\n        _logger = loggerFactory.CreateLogger<PollyResiliencePipelineDelegatingHandler>();\n    }\n\n    private IPollyQoSResiliencePipelineProvider<HttpResponseMessage> GetQoSProvider()\n    {\n        Debug.Assert(_contextAccessor.HttpContext != null, \"_contextAccessor.HttpContext != null\");\n\n        // TODO: Move IPollyQoSResiliencePipelineProvider<HttpResponseMessage> object injection to DI container by a DI helper\n        return _contextAccessor.HttpContext.RequestServices.GetService<IPollyQoSResiliencePipelineProvider<HttpResponseMessage>>();\n    }\n\n    /// <summary>\n    /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.\n    /// </summary>\n    /// <param name=\"request\">Downstream request.</param>\n    /// <param name=\"cancellationToken\">Token to cancel the task.</param>\n    /// <returns>A <see cref=\"Task{HttpResponseMessage}\"/> object of a <see cref=\"HttpResponseMessage\"/> result.</returns>\n    /// <exception cref=\"BrokenCircuitException\">Exception thrown when a circuit is broken.</exception>\n    /// <exception cref=\"HttpRequestException\">Exception thrown by <see cref=\"HttpClient\"/> and <see cref=\"HttpMessageHandler\"/> classes.</exception>\n    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        var provider = GetQoSProvider();\n        var pipeline = provider.GetResiliencePipeline(_route);\n        if (pipeline == null)\n        {\n#if DEBUG\n            _logger.LogDebug(() => $\"No pipeline was detected by QoS provider for the route with downstream URL '{request.RequestUri}'.\");\n#endif\n            return await base.SendAsync(request, cancellationToken); // shortcut > no qos\n        }\n\n#if DEBUG\n        _logger.LogInformation(() => $\"The {pipeline} pipeline has detected by QoS provider for the route with downstream URL '{request.RequestUri}'. Going to execute request...\");\n#endif\n        return await pipeline.ExecuteAsync(async (token) => await base.SendAsync(request, token), cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/TimeoutStrategy.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.Provider.Polly;\n\n/// <summary>\n/// Polly requirements for the <see href=\"https://www.pollydocs.org/strategies/timeout.html\">Timeout resilience strategy</see>.\n/// The subject of this strategy is the <see cref=\"QoSOptions.Timeout\"/> property.\n/// </summary>\npublic static class TimeoutStrategy\n{\n    // Actual Polly's Timeout constraint -> https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout\n    public const int LowTimeout = 10; // 10 ms\n    public const int DefTimeout = 30_000; // 30 seconds\n    public const int HighTimeout = 86_400_000; // 24 hours in milliseconds\n\n    /// <summary>\n    /// Applies Polly's <see href=\"https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout\">Timeout</see> constraint to the value.\n    /// <para>If using Polly v8 or later, and in accordance with Polly's <see href=\"https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout\">Timeout</see> constraint, this value must be greater than <see cref=\"LowTimeout\"/> (10 milliseconds) and less than <see cref=\"HighTimeout\"/> (24 hours).</para></summary>\n    /// <param name=\"milliseconds\">The value in milliseconds.</param>\n    /// <returns>The same value if the constraint is satisfied; otherwise, <see langword=\"null\"/>.</returns>\n    public static int? Timeout(int milliseconds) => IsValidTimeout(milliseconds) ? milliseconds : null;\n    public static bool IsValidTimeout(this int milliseconds) => milliseconds > LowTimeout && milliseconds < HighTimeout;\n\n    /// <summary>Gets or sets the default timeout in milliseconds, which overrides Polly's default of 30 seconds.\n    /// <para>The setter enforces Polly's <see href=\"https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout\">Timeout</see> constraint that the assigned value must fall within the range (<see cref=\"LowTimeout\"/>, <see cref=\"HighTimeout\"/>).</para></summary>\n    /// <remarks>By default, initialized to <see cref=\"DefTimeout\"/> (30 seconds).</remarks>\n    /// <value>An <see cref=\"int\"/> value in milliseconds.</value>\n    public static int DefaultTimeout { get => defaultTimeout; set => defaultTimeout = Timeout(value) ?? DefTimeout; }\n    private static int defaultTimeout = DefTimeout;\n}\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\n// Project extra global namespaces\nglobal using Polly;\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n"
  },
  {
    "path": "src/Ocelot.Provider.Polly/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {},\n    \"net10.0/win-x64\": {},\n    \"net8.0\": {\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.2\",\n        \"contentHash\": \"3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {},\n    \"net8.0/win-x64\": {},\n    \"net9.0\": {\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {},\n    \"net9.0/win-x64\": {}\n  }\n}"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/.gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\r\n## files generated by popular Visual Studio add-ons.\r\n\r\n# User-specific files\r\n*.suo\r\n*.user\r\n*.userosscache\r\n*.sln.docstates\r\n\r\n# User-specific files (MonoDevelop/Xamarin Studio)\r\n*.userprefs\r\n\r\n# Build results\r\n[Dd]ebug/\r\n[Dd]ebugPublic/\r\n[Rr]elease/\r\n[Rr]eleases/\r\nx64/\r\nx86/\r\nbuild/\r\nbld/\r\n[Bb]in/\r\n[Oo]bj/\r\n\r\n# Visual Studio 2015 cache/options directory\r\n.vs/\r\n# Uncomment if you have tasks that create the project's static files in wwwroot\r\n#wwwroot/\r\n\r\n# MSTest test Results\r\n[Tt]est[Rr]esult*/\r\n[Bb]uild[Ll]og.*\r\n\r\n# NUNIT\r\n*.VisualState.xml\r\nTestResult.xml\r\n\r\n# Build Results of an ATL Project\r\n[Dd]ebugPS/\r\n[Rr]eleasePS/\r\ndlldata.c\r\n\r\n# DNX\r\nproject.lock.json\r\nartifacts/\r\n\r\n*_i.c\r\n*_p.c\r\n*_i.h\r\n*.ilk\r\n*.meta\r\n*.obj\r\n*.pch\r\n*.pdb\r\n*.pgc\r\n*.pgd\r\n*.rsp\r\n*.sbr\r\n*.tlb\r\n*.tli\r\n*.tlh\r\n*.tmp\r\n*.tmp_proj\r\n*.log\r\n*.vspscc\r\n*.vssscc\r\n.builds\r\n*.pidb\r\n*.svclog\r\n*.scc\r\n\r\n# Chutzpah Test files\r\n_Chutzpah*\r\n\r\n# Visual C++ cache files\r\nipch/\r\n*.aps\r\n*.ncb\r\n*.opendb\r\n*.opensdf\r\n*.sdf\r\n*.cachefile\r\n\r\n# Visual Studio profiler\r\n*.psess\r\n*.vsp\r\n*.vspx\r\n*.sap\r\n\r\n# TFS 2012 Local Workspace\r\n$tf/\r\n\r\n# Guidance Automation Toolkit\r\n*.gpState\r\n\r\n# ReSharper is a .NET coding add-in\r\n_ReSharper*/\r\n*.[Rr]e[Ss]harper\r\n*.DotSettings.user\r\n\r\n# JustCode is a .NET coding add-in\r\n.JustCode\r\n\r\n# TeamCity is a build add-in\r\n_TeamCity*\r\n\r\n# DotCover is a Code Coverage Tool\r\n*.dotCover\r\n\r\n# NCrunch\r\n_NCrunch_*\r\n.*crunch*.local.xml\r\nnCrunchTemp_*\r\n\r\n# MightyMoose\r\n*.mm.*\r\nAutoTest.Net/\r\n\r\n# Web workbench (sass)\r\n.sass-cache/\r\n\r\n# Installshield output folder\r\n[Ee]xpress/\r\n\r\n# DocProject is a documentation generator add-in\r\nDocProject/buildhelp/\r\nDocProject/Help/*.HxT\r\nDocProject/Help/*.HxC\r\nDocProject/Help/*.hhc\r\nDocProject/Help/*.hhk\r\nDocProject/Help/*.hhp\r\nDocProject/Help/Html2\r\nDocProject/Help/html\r\n\r\n# Click-Once directory\r\npublish/\r\n\r\n# Publish Web Output\r\n*.[Pp]ublish.xml\r\n*.azurePubxml\r\n# TODO: Comment the next line if you want to checkin your web deploy settings\r\n# but database connection strings (with potential passwords) will be unencrypted\r\n*.pubxml\r\n*.publishproj\r\n\r\n# NuGet Packages\r\n*.nupkg\r\n# The packages folder can be ignored because of Package Restore\r\n**/packages/*\r\n# except build/, which is used as an MSBuild target.\r\n!**/packages/build/\r\n# Uncomment if necessary however generally it will be regenerated when needed\r\n#!**/packages/repositories.config\r\n\r\n# Microsoft Azure Build Output\r\ncsx/\r\n*.build.csdef\r\n\r\n# Microsoft Azure Emulator\r\necf/\r\nrcf/\r\n\r\n# Microsoft Azure ApplicationInsights config file\r\nApplicationInsights.config\r\n\r\n# Windows Store app package directory\r\nAppPackages/\r\nBundleArtifacts/\r\n\r\n# Visual Studio cache files\r\n# files ending in .cache can be ignored\r\n*.[Cc]ache\r\n# but keep track of directories ending in .cache\r\n!*.[Cc]ache/\r\n\r\n# Others\r\nClientBin/\r\n~$*\r\n*~\r\n*.dbmdl\r\n*.dbproj.schemaview\r\n*.pfx\r\n*.publishsettings\r\nnode_modules/\r\norleans.codegen.cs\r\n\r\n# RIA/Silverlight projects\r\nGenerated_Code/\r\n\r\n# Backup & report files from converting an old project file\r\n# to a newer Visual Studio version. Backup files are not needed,\r\n# because we have git ;-)\r\n_UpgradeReport_Files/\r\nBackup*/\r\nUpgradeLog*.XML\r\nUpgradeLog*.htm\r\n\r\n# SQL Server files\r\n*.mdf\r\n*.ldf\r\n\r\n# Business Intelligence projects\r\n*.rdl.data\r\n*.bim.layout\r\n*.bim_*.settings\r\n\r\n# Microsoft Fakes\r\nFakesAssemblies/\r\n\r\n# GhostDoc plugin setting file\r\n*.GhostDoc.xml\r\n\r\n# Node.js Tools for Visual Studio\r\n.ntvs_analysis.dat\r\n\r\n# Visual Studio 6 build log\r\n*.plg\r\n\r\n# Visual Studio 6 workspace options file\r\n*.opt\r\n\r\n# Visual Studio LightSwitch build output\r\n**/*.HTMLClient/GeneratedArtifacts\r\n**/*.DesktopClient/GeneratedArtifacts\r\n**/*.DesktopClient/ModelManifest.xml\r\n**/*.Server/GeneratedArtifacts\r\n**/*.Server/ModelManifest.xml\r\n_Pvt_Extensions\r\n\r\n# Paket dependency manager\r\n.paket/paket.exe\r\n\r\n# FAKE - F# Make\r\n.fake/\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Administration/AdministrationSteps.cs",
    "content": "﻿using Ocelot.AcceptanceTests.Authentication;\nusing Ocelot.Testing.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Administration;\n\npublic sealed class AdministrationSteps : AuthenticationSteps\n{\n    private Task GivenThereIsOcelotInternalJwtAuthServiceRunning(CancellationToken token)\n    {\n        var scopes = new string[] { OcelotScopes.Api, OcelotScopes.Api2 };\n        var jwtSigningServer = CreateJwtSigningServer(JwtSigningServerUrl, scopes);\n        return jwtSigningServer.StartAsync(token)\n            .ContinueWith(t => VerifyJwtSigningServerStarted(JwtSigningServerUrl, token), token);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Administration/CacheManagerTests.cs",
    "content": "//using Ocelot.Administration;\n//x using CacheManager.Core;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.AcceptanceTests.Authentication;\n//x using Ocelot.Cache.CacheManager;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Testing.Authentication;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Administration;\n\npublic sealed class CacheManagerTests : AuthenticationSteps\n{\n    public CacheManagerTests() : base()\n    { }\n\n    [Fact(\n        DisplayName = \"TODO \" + nameof(ShouldClearCacheRegionViaAdministrationAPI),\n        Skip = \"TODO: Requires redevelopment after deprecation of Ocelot.Administration.IdentityServer4 package.\")]\n    public async Task ShouldClearCacheRegionViaAdministrationAPI()\n    {\n        //int port = PortFinder.GetRandomPort();\n        //var ocelotUrl = DownstreamUrl(port);\n        var configuration = new FileConfiguration\n        {\n            Routes = [\n                GivenRoute(),\n                GivenRoute(\"/test\"),\n            ],\n            GlobalConfiguration = new()\n            {\n                //BaseUrl = ocelotUrl,\n            },\n        };\n        //GivenThereIsAConfiguration(configuration);\n        const string AdminPath = \"/administration\";\n\n        //GivenOcelotIsRunning(s => WithCacheManagerAndAdministrationForExternalJwtServer(s, AdminPath));\n        var port = await GivenOcelotHostIsRunning(\n            WithBasicConfiguration, // Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate,\n            s => WithCacheManagerAndAdministrationForExternalJwtServer(s, AdminPath), // Action<IServiceCollection> configureServices,\n            WithUseOcelot, // Action<IApplicationBuilder> configureApp,\n            null, null, null, null);\n        bool isExternal = true;\n        await GivenThereIsExternalJwtSigningService([OcelotScopes.OcAdmin], Xunit.TestContext.Current.CancellationToken);\n        var token = await GivenIHaveATokenWithUrlPath(\n            path: !isExternal ? AdminPath : string.Empty,\n            scope: OcelotScopes.OcAdmin);\n        GivenIHaveAddedATokenToMyRequest(token);\n\n        //await WhenIGetUrlOnTheApiGateway(\"/\");\n        //ThenTheStatusCodeShouldBeOK(); // currently HttpStatusCode.BadGateway\n        response = await ocelotClient.DeleteAsync($\"{AdminPath}/outputcache/{TestName()}\", Xunit.TestContext.Current.CancellationToken);\n        ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent); // currently HttpStatusCode.Unauthorized\n    }\n\n    public static FileCacheOptions DefaultFileCacheOptions { get; set; } = new()\n    {\n        TtlSeconds = 10,\n    };\n\n    private FileRoute GivenRoute(string upstream = null, FileCacheOptions options = null) => new()\n    {\n        DownstreamHostAndPorts = [ Localhost(80) ],\n        DownstreamScheme = Uri.UriSchemeHttps,\n        DownstreamPathTemplate = \"/\",\n        UpstreamHttpMethod = [HttpMethods.Get],\n        UpstreamPathTemplate = upstream ?? \"/\",\n        FileCacheOptions = options ?? DefaultFileCacheOptions,\n    };\n\n    private void WithCacheManagerAndAdministrationForExternalJwtServer(IServiceCollection services,\n        string adminPath,\n        [CallerMemberName] string testName = nameof(CacheManagerTests))\n    {\n        //x static void WithSettings(ConfigurationBuilderCachePart settings)\n        //x {\n        //x    settings.WithDictionaryHandle();\n        //x }\n        services.AddMvc(option => option.EnableEndpointRouting = false);\n        services.AddOcelot()\n            //x.AddCacheManager(WithSettings)\n\n            //.AddAdministration(adminPath, \"secret\") // this is for internal server\n            .AddAdministration(adminPath, testName,\n                externalJwtServer: new Uri(JwtSigningServerUrl)); // this is for external server\n    }\n\n    public override void Dispose()\n    {\n        Environment.SetEnvironmentVariable(\"OCELOT_CERTIFICATE\", string.Empty);\n        Environment.SetEnvironmentVariable(\"OCELOT_CERTIFICATE_PASSWORD\", string.Empty);\n        base.Dispose();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Administration/OcelotBuilderExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Administration;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Middleware;\nusing System;\nusing System.Collections.Generic;\nusing System.IdentityModel.Tokens.Jwt;\nusing System.Linq;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace Ocelot.AcceptanceTests.Administration;\n\npublic static class OcelotBuilderExtensions\n{\n    public static IOcelotBuilder AddAdministration(this IOcelotBuilder builder, string path, string apiSecret,\n        Action<JwtBearerOptions> configureOptions = null, Uri externalJwtServer = null)\n    {\n        var administrationPath = new AdministrationPath(path, apiSecret, externalJwtServer);\n        builder.Services\n            .AddSingleton<IAdministrationPath>(administrationPath)\n            .AddSingleton<OcelotMiddlewareConfigurationDelegate>(GetOcelotMiddlewareConfiguration);\n\n        //var jwtServerConfiguration = GetIdentityServerConfiguration(secret);\n        //AddIdentityServer(identityServerConfiguration, administrationPath, builder, builder.Configuration);\n        var authBuilder = builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);\n        authBuilder = configureOptions is not null\n            ? authBuilder.AddJwtBearer(configureOptions)\n            : authBuilder.AddJwtBearer();\n        return builder;\n    }\n\n    public static Task GetOcelotMiddlewareConfiguration(IApplicationBuilder builder)\n    {\n        var repo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();\n        var config = repo.Get();\n        var administrationPath = config?.Data?.AdministrationPath;\n        var administration = builder.ApplicationServices.GetService<IAdministrationPath>();\n        if (administration.ExternalJwtSigningUrl != null)\n        {\n            builder.UseOcelotJwtServer(administration.ExternalJwtSigningUrl); // UseIdentityServer();\n        }\n        if (administrationPath.IsNotEmpty() && administration.Path.IsNotEmpty())\n        {\n            builder.Map(administrationPath, AddOcelotAdministrationControllers);\n        }\n        return Task.CompletedTask;\n    }\n\n    public static void AddOcelotAdministrationControllers(this IApplicationBuilder builder) => builder\n        .UseAuthentication()\n        .UseRouting()\n        .UseAuthorization()\n        .UseEndpoints(endpoints =>\n        {\n            endpoints.MapDefaultControllerRoute();\n            endpoints.MapControllers();\n        });\n\n    public static IApplicationBuilder UseOcelotJwtServer(this IApplicationBuilder app, Uri externalJwtSigningUrl, bool requireInstance = false)\n    {\n        ArgumentNullException.ThrowIfNull(app);\n\n        //app.Properties[AuthenticationMiddlewareSetKey] = true;\n        //return app.UseMiddleware<AuthenticationMiddleware>();\n        return app;\n    }\n\n    public static IdentityServerConfiguration GetIdentityServerConfiguration(string secret)\n    {\n        var credentialsSigningCertificateLocation = Environment.GetEnvironmentVariable(\"OCELOT_CERTIFICATE\");\n        var credentialsSigningCertificatePassword = Environment.GetEnvironmentVariable(\"OCELOT_CERTIFICATE_PASSWORD\");\n\n        return new IdentityServerConfiguration(\n            \"admin\",\n            false,\n            secret,\n            new List<string> { \"admin\", \"openid\", \"offline_access\" },\n            credentialsSigningCertificateLocation,\n            credentialsSigningCertificatePassword\n        );\n    }\n\n    public static IOcelotBuilder AddAdministration(this IOcelotBuilder builder, string path, Action<JwtBearerOptions> configureOptions)\n    {\n        //var administrationPath = new AdministrationPath(path);\n        //builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get);\n        //if (configureOptions != null)\n        //{\n        //    AddIdentityServer(configureOptions, builder);\n        //}\n\n        //builder.Services.AddSingleton<IAdministrationPath>(administrationPath);\n        return builder;\n    }\n    /*\n    private static void AddIdentityServer(Action<JwtBearerOptions> configOptions, IOcelotBuilder builder)\n    {\n        builder.Services\n            .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)\n            .AddJwtBearer(\"Bearer\", configOptions);\n    }\n\n    private static void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath, IOcelotBuilder builder, IConfiguration configuration)\n    {\n        builder.Services.TryAddSingleton(identityServerConfiguration);\n        var identityServerBuilder = builder.Services\n            .AddIdentityServer(o =>\n            {\n                o.IssuerUri = \"Ocelot\";\n                o.EmitStaticAudienceClaim = true;\n            })\n            .AddInMemoryApiScopes(ApiScopes(identityServerConfiguration))\n            .AddInMemoryApiResources(Resources(identityServerConfiguration))\n            .AddInMemoryClients(Client(identityServerConfiguration));\n\n        var urlFinder = new BaseUrlFinder(configuration);\n        var baseSchemeUrlAndPort = urlFinder.Find();\n        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();\n\n        builder.Services\n            .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)\n            .AddJwtBearer(\"Bearer\", options =>\n            {\n                options.Authority = baseSchemeUrlAndPort + adminPath.Path;\n                options.RequireHttpsMetadata = identityServerConfiguration.RequireHttps;\n\n                options.TokenValidationParameters = new TokenValidationParameters\n                {\n                    ValidateAudience = false,\n                };\n            });\n\n        //todo - refactor naming..\n        if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword))\n        {\n            identityServerBuilder.AddDeveloperSigningCredential();\n        }\n        else\n        {\n            //todo - refactor so calls method?\n            var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);\n            identityServerBuilder.AddSigningCredential(cert);\n        }\n    }*/\n}\n\npublic class IdentityServerConfiguration\n{\n    public IdentityServerConfiguration(\n        string apiName,\n        bool requireHttps,\n        string apiSecret,\n        List<string> allowedScopes,\n        string credentialsSigningCertificateLocation,\n        string credentialsSigningCertificatePassword)\n    {\n        ApiName = apiName;\n        RequireHttps = requireHttps;\n        ApiSecret = apiSecret;\n        AllowedScopes = allowedScopes;\n        CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation;\n        CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword;\n    }\n\n    public string ApiName { get; }\n    public bool RequireHttps { get; }\n    public List<string> AllowedScopes { get; }\n    public string ApiSecret { get; }\n    public string CredentialsSigningCertificateLocation { get; }\n    public string CredentialsSigningCertificatePassword { get; }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/AggregateTests.cs",
    "content": "//using IdentityServer4.AccessTokenValidation;\r\n//using IdentityServer4.Extensions;\r\n//using IdentityServer4.Models;\r\nusing Microsoft.AspNetCore.Authentication;\r\nusing Microsoft.AspNetCore.Authorization;\r\nusing Microsoft.AspNetCore.Builder;\r\nusing Microsoft.AspNetCore.Hosting;\r\nusing Microsoft.AspNetCore.Http;\r\nusing Microsoft.AspNetCore.Mvc.Authorization;\r\nusing Microsoft.AspNetCore.TestHost;\r\nusing Microsoft.Extensions.Configuration;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.AcceptanceTests.Authentication;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.DependencyInjection;\r\nusing Ocelot.Middleware;\r\nusing Ocelot.Multiplexer;\r\nusing System.Text;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class AggregateTests : Steps\r\n{\r\n    private readonly string[] _downstreamPaths;\r\n\r\n    public AggregateTests()\r\n    {\r\n        _downstreamPaths = new string[3];\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Issue\", \"597\")]\r\n    public void Should_fix_issue_597()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/api/values?MailId={userid}\",\r\n                    UpstreamPathTemplate = \"/key1data/{userid}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port,\r\n                        },\r\n                    },\r\n                    Key = \"key1\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/api/values?MailId={userid}\",\r\n                    UpstreamPathTemplate = \"/key2data/{userid}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port,\r\n                        },\r\n                    },\r\n                    Key = \"key2\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/api/values?MailId={userid}\",\r\n                    UpstreamPathTemplate = \"/key3data/{userid}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port,\r\n                        },\r\n                    },\r\n                    Key = \"key3\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/api/values?MailId={userid}\",\r\n                    UpstreamPathTemplate = \"/key4data/{userid}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port,\r\n                        },\r\n                    },\r\n                    Key = \"key4\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    RouteKeys = [\"key1\", \"key2\", \"key3\", \"key4\"],\r\n                    UpstreamPathTemplate = \"/EmpDetail/IN/{userid}\",\r\n                },\r\n                new FileAggregateRoute\r\n                {\r\n                    RouteKeys = [\"key1\", \"key2\"],\r\n                    UpstreamPathTemplate = \"/EmpDetail/US/{userid}\",\r\n                },\r\n            },\r\n            GlobalConfiguration = new FileGlobalConfiguration\r\n            {\r\n                RequestIdKey = \"CorrelationID\",\r\n            },\r\n        };\r\n\r\n        var expected = \"{\\\"key1\\\":some_data,\\\"key2\\\":some_data}\";\r\n        this.Given(x => x.GivenServiceIsRunning(port, HttpStatusCode.OK, \"some_data\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/EmpDetail/US/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_advanced_aggregate_configs()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var port3 = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port1,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/Comments\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Comments\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/users/{userId}\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port2,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/UserDetails/{userId}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"UserDetails\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/posts/{postId}\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port3,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/PostDetails/{postId}\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"PostDetails\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    UpstreamPathTemplate = \"/\",\r\n                    UpstreamHost = \"localhost\",\r\n                    RouteKeys = [\"Comments\", \"UserDetails\", \"PostDetails\"],\r\n                    RouteKeysConfig = new()\r\n                    {\r\n                        new AggregateRouteConfig\r\n                            { RouteKey = \"UserDetails\", JsonPath = \"$[*].writerId\", Parameter = \"userId\" },\r\n                        new AggregateRouteConfig\r\n                            { RouteKey = \"PostDetails\", JsonPath = \"$[*].postId\", Parameter = \"postId\" },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        var userDetailsResponseContent = @\"{\"\"id\"\":1,\"\"firstName\"\":\"\"abolfazl\"\",\"\"lastName\"\":\"\"rajabpour\"\"}\";\r\n        var postDetailsResponseContent = @\"{\"\"id\"\":1,\"\"title\"\":\"\"post1\"\"}\";\r\n        var commentsResponseContent = @\"[{\"\"id\"\":1,\"\"writerId\"\":1,\"\"postId\"\":2,\"\"text\"\":\"\"text1\"\"},{\"\"id\"\":2,\"\"writerId\"\":1,\"\"postId\"\":2,\"\"text\"\":\"\"text2\"\"}]\";\r\n\r\n        var expected = \"{\\\"Comments\\\":\" + commentsResponseContent + \",\\\"UserDetails\\\":\" + userDetailsResponseContent + \",\\\"PostDetails\\\":\" + postDetailsResponseContent + \"}\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 200, commentsResponseContent))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/users/1\", 200, userDetailsResponseContent))\r\n            .Given(x => x.GivenServiceIsRunning(2, port3, \"/posts/2\", 200, postDetailsResponseContent))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url_user_defined_aggregate()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port1,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/laura\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Laura\",\r\n                },\r\n\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port2,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/tom\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Tom\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    UpstreamPathTemplate = \"/\",\r\n                    UpstreamHost = \"localhost\",\r\n                    RouteKeys = [\"Laura\", \"Tom\"],\r\n                    Aggregator = \"FakeDefinedAggregator\",\r\n                },\r\n            },\r\n        };\r\n\r\n        var expected = \"Bye from Laura, Bye from Tom\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 200, \"{Hello from Laura}\"))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/\", 200, \"{Hello from Tom}\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi<FakeDefinedAggregator, FakeDep>())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenRoute(port1, \"/laura\", \"Laura\");\r\n        var route2 = GivenRoute(port2, \"/tom\", \"Tom\");\r\n        var configuration = GivenConfiguration(route1, route2);\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 200, \"{Hello from Laura}\"))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/\", 200, \"{Hello from Tom}\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"{\\\"Laura\\\":{Hello from Laura},\\\"Tom\\\":{Hello from Tom}}\"))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url_one_service_404()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port1,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/laura\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Laura\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port2,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/tom\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Tom\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    UpstreamPathTemplate = \"/\",\r\n                    UpstreamHost = \"localhost\",\r\n                    RouteKeys = [\"Laura\", \"Tom\"],\r\n                },\r\n            },\r\n        };\r\n\r\n        var expected = \"{\\\"Laura\\\":,\\\"Tom\\\":{Hello from Tom}}\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 404, \"\"))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/\", 200, \"{Hello from Tom}\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url_both_service_404()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port1,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/laura\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Laura\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port2,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/tom\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Tom\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    UpstreamPathTemplate = \"/\",\r\n                    UpstreamHost = \"localhost\",\r\n                    RouteKeys = [\"Laura\", \"Tom\"],\r\n                },\r\n            },\r\n        };\r\n\r\n        var expected = \"{\\\"Laura\\\":,\\\"Tom\\\":}\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 404, \"\"))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/\", 404, \"\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_be_thread_safe()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new()\r\n            {\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port1,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/laura\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Laura\",\r\n                },\r\n                new FileRoute\r\n                {\r\n                    DownstreamPathTemplate = \"/\",\r\n                    DownstreamScheme = \"http\",\r\n                    DownstreamHostAndPorts = new()\r\n                    {\r\n                        new FileHostAndPort\r\n                        {\r\n                            Host = \"localhost\",\r\n                            Port = port2,\r\n                        },\r\n                    },\r\n                    UpstreamPathTemplate = \"/tom\",\r\n                    UpstreamHttpMethod = [\"Get\"],\r\n                    Key = \"Tom\",\r\n                },\r\n            },\r\n            Aggregates = new()\r\n            {\r\n                new FileAggregateRoute\r\n                {\r\n                    UpstreamPathTemplate = \"/\",\r\n                    UpstreamHost = \"localhost\",\r\n                    RouteKeys = [\"Laura\", \"Tom\"],\r\n                },\r\n            },\r\n        };\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/\", 200, \"{Hello from Laura}\"))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/\", 200, \"{Hello from Tom}\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIMakeLotsOfDifferentRequestsToTheApiGateway())\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void WhenIMakeLotsOfDifferentRequestsToTheApiGateway()\r\n    {\r\n        var numberOfRequests = 100;\r\n        var aggregateUrl = \"/\";\r\n        var aggregateExpected = \"{\\\"Laura\\\":{Hello from Laura},\\\"Tom\\\":{Hello from Tom}}\";\r\n        var tomUrl = \"/tom\";\r\n        var tomExpected = \"{Hello from Tom}\";\r\n        var lauraUrl = \"/laura\";\r\n        var lauraExpected = \"{Hello from Laura}\";\r\n\r\n        var aggregateTasks = new Task[numberOfRequests];\r\n        for (var i = 0; i < numberOfRequests; i++)\r\n        {\r\n            aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random);\r\n        }\r\n\r\n        var tomTasks = new Task[numberOfRequests];\r\n        for (var i = 0; i < numberOfRequests; i++)\r\n        {\r\n            tomTasks[i] = Fire(tomUrl, tomExpected, random);\r\n        }\r\n\r\n        var lauraTasks = new Task[numberOfRequests];\r\n        for (var i = 0; i < numberOfRequests; i++)\r\n        {\r\n            lauraTasks[i] = Fire(lauraUrl, lauraExpected, random);\r\n        }\r\n\r\n        Task.WaitAll(lauraTasks);\r\n        Task.WaitAll(tomTasks);\r\n        Task.WaitAll(aggregateTasks);\r\n    }\r\n\r\n    private async Task Fire(string url, string expectedBody, Random random)\r\n    {\r\n        var request = new HttpRequestMessage(new HttpMethod(\"GET\"), url);\r\n        await Task.Delay(random.Next(0, 2));\r\n        var response = await ocelotClient.SendAsync(request);\r\n        var content = await response.Content.ReadAsStringAsync();\r\n        content.ShouldBe(expectedBody);\r\n    }\r\n\r\n    //[Fact]\r\n    //[Trait(\"Bug\", \"1396\")]\r\n    //public void Should_return_response_200_with_user_forwarding()\r\n    //{\r\n    //    var port1 = PortFinder.GetRandomPort();\r\n    //    var port2 = PortFinder.GetRandomPort();\r\n    //    var port3 = PortFinder.GetRandomPort();\r\n    //    var route1 = GivenRouteWithKey(port1, \"/laura\", \"Laura\");\r\n    //    var route2 = GivenRouteWithKey(port2, \"/tom\", \"Tom\");\r\n    //    var configuration = GivenConfiguration(route1, route2);\r\n    //    var identityServerUrl = $\"{Uri.UriSchemeHttp}://localhost:{port3}\";\r\n    //    void configureOptions(IdentityServerAuthenticationOptions o)\r\n    //    {\r\n    //        o.Authority = identityServerUrl;\r\n    //        o.ApiName = \"api\";\r\n    //        o.RequireHttpsMetadata = false;\r\n    //        o.SupportedTokens = SupportedTokens.Both;\r\n    //        o.ApiSecret = \"secret\";\r\n    //        o.ForwardDefault = IdentityServerAuthenticationDefaults.AuthenticationScheme;\r\n    //    }\r\n    //    Action<IServiceCollection> configureServices = s =>\r\n    //    {\r\n    //        s.AddOcelot();\r\n    //        s.AddMvcCore(mvc =>\r\n    //        {\r\n    //            var policy = new AuthorizationPolicyBuilder()\r\n    //                .RequireAuthenticatedUser()\r\n    //                .RequireClaim(\"scope\", \"api\")\r\n    //                .Build();\r\n    //            mvc.Filters.Add(new AuthorizeFilter(policy));\r\n    //        });\r\n    //        s.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)\r\n    //            .AddIdentityServerAuthentication(configureOptions);\r\n    //    };\r\n    //    var count = 0;\r\n    //    var actualContexts = new HttpContext[2];\r\n    //    Action<IApplicationBuilder> configureApp = async (app) =>\r\n    //    {\r\n    //        var configuration = new OcelotPipelineConfiguration\r\n    //        {\r\n    //            PreErrorResponderMiddleware = async (context, next) =>\r\n    //            {\r\n    //                var auth = await context.AuthenticateAsync();\r\n    //                context.User = (auth.Succeeded && auth.Principal?.IsAuthenticated() == true)\r\n    //                    ? auth.Principal : null;\r\n    //                await next.Invoke();\r\n    //            },\r\n    //            AuthorizationMiddleware = (context, next) =>\r\n    //            {\r\n    //                actualContexts[count++] = context;\r\n    //                return next.Invoke();\r\n    //            },\r\n    //        };\r\n    //        await app.UseOcelot(configuration);\r\n    //    };\r\n    //    using (var auth = new AuthenticationTests())\r\n    //    {\r\n    //        this.Given(x => auth.GivenThereIsAnIdentityServerOn(identityServerUrl, AccessTokenType.Jwt))\r\n    //            .And(x => x.GivenServiceIsRunning(0, port1, \"/\", 200, \"{Hello from Laura}\"))\r\n    //            .And(x => x.GivenServiceIsRunning(1, port2, \"/\", 200, \"{Hello from Tom}\"))\r\n    //            .And(x => auth.GivenToken(identityServerUrl))\r\n    //            .And(x => auth.GivenThereIsAConfiguration(configuration))\r\n    //            .And(x => auth.GivenOcelotIsRunning(configureServices, configureApp))\r\n    //            .And(x => auth.GivenIHaveAddedATokenToMyRequest())\r\n    //            .When(x => auth.WhenIGetUrlOnTheApiGatewayWithRequestId(\"/\"))\r\n    //            .Then(x => auth.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n    //            .And(x => auth.ThenTheResponseBodyShouldBe(\"{\\\"Laura\\\":{Hello from Laura},\\\"Tom\\\":{Hello from Tom}}\"))\r\n    //            .And(x => x.ThenTheDownstreamUrlPathShouldBe(\"/\", \"/\"))\r\n    //            .BDDfy();\r\n    //    }\r\n\r\n    //    // Assert\r\n    //    for (var i = 0; i < actualContexts.Length; i++)\r\n    //    {\r\n    //        var ctx = actualContexts[i].ShouldNotBeNull();\r\n    //        ctx.Items.DownstreamRoute().Key.ShouldBe(configuration.Routes[i].Key);\r\n    //        var user = ctx.User.ShouldNotBeNull();\r\n    //        user.IsAuthenticated().ShouldBeTrue();\r\n    //        user.Claims.Count().ShouldBeGreaterThan(1);\r\n    //        user.Claims.FirstOrDefault(c => c is { Type: \"scope\", Value: \"api\" }).ShouldNotBeNull();\r\n    //    }\r\n    //}\r\n    [Fact]\r\n    [Trait(\"Bug\", \"2039\")]\r\n    public void Should_return_response_200_with_copied_body_sent_on_multiple_services()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenRoute(port1, \"/Service1\", \"Service1\", \"/Sub1\");\r\n        var route2 = GivenRoute(port2, \"/Service2\", \"Service2\", \"/Sub2\");\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        var requestBody = @\"{\"\"id\"\":1,\"\"response\"\":\"\"fromBody-#REPLACESTRING#\"\"}\";\r\n        var sub1ResponseContent = @\"{\"\"id\"\":1,\"\"response\"\":\"\"fromBody-s1\"\"}\";\r\n        var sub2ResponseContent = @\"{\"\"id\"\":1,\"\"response\"\":\"\"fromBody-s2\"\"}\";\r\n        var expected = $\"{{\\\"Service1\\\":{sub1ResponseContent},\\\"Service2\\\":{sub2ResponseContent}}}\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/Sub1\", 200, reqBody => reqBody.Replace(\"#REPLACESTRING#\", \"s1\")))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/Sub2\", 200, reqBody => reqBody.Replace(\"#REPLACESTRING#\", \"s2\")))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGatewayWithBody(\"/\", requestBody))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"2039\")]\r\n    public void Should_return_response_200_with_copied_form_sent_on_multiple_services()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenRoute(port1, \"/Service1\", \"Service1\", \"/Sub1\");\r\n        var route2 = GivenRoute(port2, \"/Service2\", \"Service2\", \"/Sub2\");\r\n        var configuration = GivenConfiguration(route1, route2);\r\n\r\n        var formValues = new[]\r\n        {\r\n            new KeyValuePair<string, string>(\"param1\", \"value1\"),\r\n            new KeyValuePair<string, string>(\"param2\", \"from-form-REPLACESTRING\"),\r\n        };\r\n\r\n        var sub1ResponseContent = \"\\\"[key:param1=value1&param2=from-form-s1]\\\"\";\r\n        var sub2ResponseContent = \"\\\"[key:param1=value1&param2=from-form-s2]\\\"\";\r\n        var expected = $\"{{\\\"Service1\\\":{sub1ResponseContent},\\\"Service2\\\":{sub2ResponseContent}}}\";\r\n\r\n        this.Given(x => x.GivenServiceIsRunning(0, port1, \"/Sub1\", 200, reqForm => FormatFormCollection(reqForm).Replace(\"REPLACESTRING\", \"s1\")))\r\n            .Given(x => x.GivenServiceIsRunning(1, port2, \"/Sub2\", 200, reqForm => FormatFormCollection(reqForm).Replace(\"REPLACESTRING\", \"s2\")))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGatewayWithForm(\"/\", \"key\", formValues))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(expected))\r\n            .BDDfy();\r\n    }\r\n\r\n    private static string FormatFormCollection(IFormCollection reqForm)\r\n    {\r\n        var sb = new StringBuilder()\r\n            .Append('\"');\r\n\r\n        foreach (var kvp in reqForm)\r\n        {\r\n            sb.Append($\"[{kvp.Key}:{kvp.Value}]\");\r\n        }\r\n\r\n        return sb\r\n            .Append('\"')\r\n            .ToString();\r\n    }\r\n\r\n    private void GivenServiceIsRunning(int port, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, context =>\r\n        {\r\n            context.Response.StatusCode = (int)statusCode;\r\n            return context.Response.WriteAsync(responseBody);\r\n        });\r\n    }\r\n\r\n    private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody)\r\n        => GivenServiceIsRunning(index, port, basePath, statusCode,\r\n            async context =>\r\n            {\r\n                await context.Response.WriteAsync(responseBody);\r\n            });\r\n\r\n    private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func<string, string> responseFromBody)\r\n        => GivenServiceIsRunning(index, port, basePath, statusCode,\r\n            async context =>\r\n            {\r\n                var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync();\r\n                var responseBody = responseFromBody(requestBody);\r\n                await context.Response.WriteAsync(responseBody);\r\n            });\r\n\r\n    private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func<IFormCollection, string> responseFromForm)\r\n        => GivenServiceIsRunning(index, port, basePath, statusCode,\r\n            async context =>\r\n            {\r\n                var responseBody = responseFromForm(context.Request.Form);\r\n                await context.Response.WriteAsync(responseBody);\r\n            });\r\n\r\n    private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action<HttpContext> processContext)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\r\n        {\r\n            _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value)\r\n                ? context.Request.PathBase.Value\r\n                : context.Request.Path.Value;\r\n\r\n            if (_downstreamPaths[index] != basePath)\r\n            {\r\n                context.Response.StatusCode = (int)HttpStatusCode.NotFound;\r\n                await context.Response.WriteAsync(\"downstream path doesn't match base path\");\r\n            }\r\n            else\r\n            {\r\n                context.Response.StatusCode = statusCode;\r\n                processContext?.Invoke(context);\r\n            }\r\n        });\r\n    }\r\n\r\n    private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi<TAggregator, TDependency>()\r\n        where TAggregator : class, IDefinedAggregator\r\n        where TDependency : class\r\n    {\r\n        static void WithSpecificAggregators(IServiceCollection services) => services\r\n            .AddSingleton<TDependency>()\r\n            .AddOcelot()\r\n            .AddSingletonDefinedAggregator<TAggregator>();\r\n        GivenOcelotIsRunning(WithSpecificAggregators);\r\n    }\r\n\r\n    private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath)\r\n    {\r\n        _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne);\r\n        _downstreamPaths[1].ShouldBe(expectedDownstreamPath);\r\n    }\r\n\r\n    private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new()\r\n    {\r\n        DownstreamPathTemplate = downstream ?? \"/\",\r\n        DownstreamScheme = Uri.UriSchemeHttp,\r\n        DownstreamHostAndPorts = new() { new(\"localhost\", port) },\r\n        UpstreamPathTemplate = upstream,\r\n        UpstreamHttpMethod = [HttpMethods.Get],\r\n        Key = key,\r\n    };\r\n\r\n    public override FileConfiguration GivenConfiguration(params FileRoute[] routes)\r\n    {\r\n        var conf = base.GivenConfiguration(routes);\r\n        conf.Aggregates.Add(\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = new(routes.Select(r => r.Key)), // [ \"Laura\", \"Tom\" ],\r\n            }\r\n        );\r\n        return conf;\r\n    }\r\n}\r\n\r\npublic class FakeDep\r\n{\r\n}\r\n\r\npublic class FakeDefinedAggregator : IDefinedAggregator\r\n{\r\n    public FakeDefinedAggregator(FakeDep dep)\r\n    {\r\n    }\r\n\r\n    public async Task<DownstreamResponse> Aggregate(List<HttpContext> responses)\r\n    {\r\n        var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync();\r\n        var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync();\r\n\r\n        var merge = $\"{one}, {two}\";\r\n        merge = merge.Replace(\"Hello\", \"Bye\").Replace(\"{\", \"\").Replace(\"}\", \"\");\r\n        var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList();\r\n        return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, \"some reason\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs",
    "content": "using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Testing.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Authentication;\n\npublic sealed class AuthenticationTests : AuthenticationSteps\n{\n    public AuthenticationTests()\n    { }\n\n    [Fact]\n    public void Should_return_401_using_identity_server_access_token()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, method: HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty<string>(), Xunit.TestContext.Current.CancellationToken))\n           .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty))\n           .And(x => GivenThereIsAConfiguration(configuration))\n           .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n           .When(x => WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\"))\n           .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized))\n           .BDDfy();\n    }\n\n    [Fact]\n    public async Task Should_return_response_200_using_identity_server()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken);\n        await GivenIHaveAToken();\n        GivenIHaveAddedATokenToMyRequest();\n\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n        ThenTheResponseBodyShouldBe(\"Hello from Laura\");\n    }\n\n    [Fact]\n    public async Task Should_return_response_401_using_identity_server_with_token_requested_for_other_api()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\");\n        GivenThereIsAConfiguration(configuration);\n\n        static void WithOtherApiAudience(JwtBearerOptions o)\n        {\n            o.Audience = \"other.api.com\";\n            o.TokenValidationParameters.ValidAudience = \"other.api.com\";\n        }\n        void WithOtherApiBearerAuthentication(IServiceCollection services)\n        {\n            services.AddOcelot();\n            Action<JwtBearerOptions> configureOptions = WithThreemammalsOptions;\n            services.AddAuthentication().AddJwtBearer(configureOptions + WithOtherApiAudience);\n        }\n        GivenOcelotIsRunning(WithOtherApiBearerAuthentication);\n\n        await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken);\n        var token = await GivenIHaveAToken(scope: \"api2\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized);\n    }\n\n    [Fact]\n    public async Task Should_return_201_using_identity_server_access_token()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, method: HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken);\n        await GivenIHaveAToken();\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.Created);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\n    [Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\n    [InlineData(true, HttpStatusCode.OK)]\n    [InlineData(false, HttpStatusCode.Unauthorized)]\n    public async Task Should_use_global_authentication(bool hasToken, HttpStatusCode status)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        route.AuthenticationOptions.AuthenticationProviderKeys = []; // no route auth!\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration = GivenGlobalAuthConfiguration();\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK);\n        await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken);\n        if (hasToken)\n        {\n            await GivenIHaveAToken();\n            GivenIHaveAddedATokenToMyRequest();\n        }\n\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(status);\n        ThenTheResponseBodyShouldBe(hasToken ? Body() : string.Empty);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\n    [Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\n    public async Task Should_allow_anonymous_route_and_return_200_when_global_auth_options_and_no_token()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, allowAnonymous: true);\n        route.AuthenticationOptions.AuthenticationProviderKeys = [];\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration = GivenGlobalAuthConfiguration();\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK);\n        await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken);\n\n        // await GivenIHaveAToken();\n        // GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBody();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n    [Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\n    public async Task ShouldApplyGlobalAuthenticationOptions_ForStaticRoutes()\n    {\n        var ports = PortFinder.GetPorts(3);\n        var route1 = GivenAuthRoute(ports[0], \"/route1\",\n            options: null); // no opts -> use global opts\n        var route2 = GivenAuthRoute(ports[1], \"/route2\",\n            GivenOptions(false, [\"api\"], [\"test\", JwtBearerDefaults.AuthenticationScheme]));\n        var route3 = GivenAuthRoute(ports[2], \"/noAuthorization\",\n            GivenOptions(false, [\"invalid-scope\"]));\n        var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection\n        var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions\n            = new(GivenOptions(false, [\"apiGlobal\"], [JwtBearerDefaults.AuthenticationScheme]));\n\n        GivenThereIsAServiceRunningOnPath(ports[0], \"/route1\");\n        GivenThereIsAServiceRunningOnPath(ports[1], \"/route2\");\n        GivenThereIsAServiceRunningOnPath(ports[2], \"/noAuthorization\");\n        GivenThereIsAConfiguration(configuration);\n        Action<IServiceCollection> withAuth = WithJwtBearerAuthentication;\n        void WithOAuthNotConfigured(IServiceCollection services) => services\n            .AddAuthentication()\n            .AddOAuth(route2.AuthenticationOptions.AuthenticationProviderKeys[0],\n                opts => opts.ClientSecret = \"bla-bla... actually, there are no options\"); // -> 'test' scheme and it is registered now, but the auth will fail\n        GivenOcelotIsRunning(withAuth + WithOAuthNotConfigured);\n        await GivenThereIsExternalJwtSigningService([\"api\", \"apiGlobal\", \"Mr.Who\"], Xunit.TestContext.Current.CancellationToken);\n\n        await GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]);\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBody();\n\n        await GivenIHaveAToken(scope: route2.AuthenticationOptions.AllowedScopes[0]);\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBody();\n\n        await GivenIHaveAToken(scope: \"Mr.Who\"); // should be different scope of route #3 which is \"invalid-scope\"\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/noAuthorization\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden);\n        await ThenTheResponseBodyShouldBeEmpty();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n    [Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\n    public async Task ShouldApplyGlobalGroupAuthenticationOptions_ForStaticRoutes_WhenRouteOptsHasAKey()\n    {\n        // 1st route\n        var ports = PortFinder.GetPorts(3);\n        var route1 = GivenAuthRoute(ports[0], \"/route1\", options: null); // no opts -> no auth at all\n        route1.Key = null; // 1st route is not in the global group\n\n        // 2nd route\n        var route2 = GivenAuthRoute(ports[1], \"/route2\", options: null); // 2nd route opts will be applied from global ones\n        route2.Key = \"R2\"; // 2nd route is in the group\n\n        // 3rd route\n        var route3 = GivenAuthRoute(ports[2], \"/noAuthorization\",\n            GivenOptions(false, [\"invalid-scope\"], [JwtBearerDefaults.AuthenticationScheme]));\n\n        var configuration = GivenConfiguration(route1, route2, route3);\n        var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions\n            = new(GivenOptions(false, [\"apiGlobal\"], [JwtBearerDefaults.AuthenticationScheme]))\n            {\n                RouteKeys = [\"R2\"],\n            };\n\n        GivenThereIsAServiceRunningOnPath(ports[0], \"/route1\");\n        GivenThereIsAServiceRunningOnPath(ports[1], \"/route2\");\n        GivenThereIsAServiceRunningOnPath(ports[2], \"/noAuthorization\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenThereIsExternalJwtSigningService([\"api\", \"apiGlobal\", \"Mr.Who\"], Xunit.TestContext.Current.CancellationToken);\n\n        await GivenIHaveAToken(scope: \"Mr.Who\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        ThenTheStatusCodeShouldBeOK(); // auth is switched off and the scope doesn't matter\n        ThenTheResponseBody();\n\n        await GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]);\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        ThenTheStatusCodeShouldBeOK(); // global scope has been accepted\n        ThenTheResponseBody();\n\n        await GivenIHaveAToken(scope: \"Mr.Who\"); // should be different scope of route #3 which is \"invalid-scope\"\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/noAuthorization\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden);\n        await ThenTheResponseBodyShouldBeEmpty();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Testing.Authentication;\nusing System.Net.Http.Headers;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Authentication;\n\n[Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n[Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n[Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\npublic sealed class MultipleAuthSchemesFeatureTests : AuthenticationSteps\n{\n    private string[] _serverUrls;\n    private BearerToken[] _tokens;\n\n    public MultipleAuthSchemesFeatureTests() : base()\n    {\n        _serverUrls = Array.Empty<string>();\n        _tokens = Array.Empty<BearerToken>();\n    }\n\n    private MultipleAuthSchemesFeatureTests Setup(int totalSchemes)\n    {\n        _serverUrls = new string[totalSchemes];\n        _tokens = new BearerToken[totalSchemes];\n        return this;\n    }\n\n    [Theory]\n    [InlineData(\"Test1\", \"Test2\")] // with multiple schemes\n    [InlineData(JwtBearerDefaults.AuthenticationScheme, \"Test\")] // with default scheme\n    [InlineData(\"Test\", JwtBearerDefaults.AuthenticationScheme)] // with default scheme\n    public async Task Should_authenticate_using_multiple_schemes(string scheme1, string scheme2)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, scheme: \"bla-bla\"); //, validScope: \"api2\"); // TODO Need further dev\n        string[] authSchemes = new[] { scheme1, scheme2 };\n        route.AuthenticationOptions.AuthenticationProviderKeys = authSchemes;\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port);\n        GivenThereIsAConfiguration(configuration);\n        Setup(authSchemes.Length);\n        _serverUrls[0] = await GivenThereIsExternalJwtSigningService([\"invalid\", \"unknown\"], Xunit.TestContext.Current.CancellationToken);\n        _serverUrls[1] = await GivenThereIsExternalJwtSigningService([\"api1\", \"api2\"], Xunit.TestContext.Current.CancellationToken);\n        GivenOcelotIsRunningWithIdentityServerAuthSchemes(\"api2\", authSchemes);\n        await GivenIHaveTokenWithScope(0, \"invalid\"); // authentication should fail because of invalid scope\n        await GivenIHaveTokenWithScope(1, \"api2\"); // authentication should succeed\n        GivenIHaveAddedAllAuthHeaders(authSchemes);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(Body());\n    }\n\n    private async Task GivenIHaveTokenWithScope(int index, string scope, [CallerMemberName] string testName = \"\")\n    {\n        string url = _serverUrls[index];\n        _tokens[index] = await GivenIHaveAToken(scope, null, url, testName);\n    }\n\n    private void GivenIHaveAddedAllAuthHeaders(string[] schemes)\n    {\n        // Assume default scheme token is attached as \"Authorization\" header, for example \"Bearer\"\n        // But default authentication setup should be ignored in multiple schemes scenario\n        ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", \"failed\");\n\n        for (int i = 0; i < schemes.Length && i < _tokens.Length; i++)\n        {\n            var token = _tokens[i];\n            var header = AuthHeaderName(schemes[i]);\n            var hvalue = new AuthenticationHeaderValue(token.TokenType, token.AccessToken);\n            GivenIAddAHeader(header, hvalue.ToString());\n        }\n    }\n\n    private static string AuthHeaderName(string scheme) => $\"Oc-{HeaderNames.Authorization}-{scheme}\";\n\n    private void WithBearerOptions(JwtBearerOptions o, string scheme, string issuerUrl)\n    {\n        AuthenticationTokenRequest request = AuthTokens[issuerUrl];\n        string authority = new Uri(JwtSigningServerUrl).Authority;\n        o.Audience = request.Audience;\n        o.Authority = authority;\n        o.RequireHttpsMetadata = false;\n        o.TokenValidationParameters = new()\n        {\n            ValidateIssuer = true,\n            ValidIssuer = authority,\n            ValidateAudience = true,\n            ValidAudience = request.Audience, // ocelotClient.BaseAddress.Authority,\n            ValidateIssuerSigningKey = true,\n            IssuerSigningKey = request.IssuerSigningKey(),\n        };\n        o.ForwardDefaultSelector = (context) => // TODO TokenRetriever ?\n        {\n            var headers = context.Request.Headers;\n            var name = AuthHeaderName(scheme);\n            if (headers.TryGetValue(name, out StringValues value))\n            {\n                // Redirect to default authentication handler which is (JwtAuthHandler) aka (Bearer)\n                headers[HeaderNames.Authorization] = value;\n                return scheme;\n            }\n\n            // Something wrong with the setup: no headers, no tokens.\n            // Redirect to default scheme to read token from default header\n            return JwtBearerDefaults.AuthenticationScheme;\n        };\n    }\n\n    private void GivenOcelotIsRunningWithIdentityServerAuthSchemes(string validScope, params string[] schemes)\n    {\n        GivenOcelotIsRunning(services =>\n        {\n            var ocelot = services.AddOcelot();\n            var auth = services.AddAuthentication(o =>\n            {\n                o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;\n                o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;\n            });\n            for (int i = 0; i < schemes.Length; i++)\n            {\n                var scheme = schemes[i];\n                var issuerUrl = _serverUrls[i];\n                auth.AddJwtBearer(scheme,\n                    o => WithBearerOptions(o, scheme, issuerUrl));\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Authorization/AuthorizationSteps.cs",
    "content": "﻿using Ocelot.Testing.Authentication;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Authorization;\n\npublic class AuthorizationSteps : AuthenticationSteps\n{\n    public void GivenIUpdateSubClaim() => AuthTokenRequesting += UpdateSubClaim;\n\n    protected virtual void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e)\n    {\n        var uid = e.Request.UserId;\n        e.Request.UserId = string.Concat(OcelotScopes.OcAdmin, \"|\", uid); // -> sub claim -> oc-sub claim\n    }\n\n    public const string DefaultAudience = null;\n    public Task<BearerToken> GivenIHaveATokenWithScope(string scope, [CallerMemberName] string testName = \"\")\n        => GivenIHaveAToken(scope, null, JwtSigningServerUrl, DefaultAudience, testName);\n    public Task<BearerToken> GivenIHaveATokenWithClaims(IEnumerable<KeyValuePair<string, string>> claims, [CallerMemberName] string testName = \"\")\n        => GivenIHaveAToken(OcelotScopes.Api, claims, JwtSigningServerUrl, DefaultAudience, testName);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Authorization/AuthorizationTests.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.Testing.Authentication;\nusing System.Security.Claims;\n\nnamespace Ocelot.AcceptanceTests.Authorization;\n\npublic sealed class AuthorizationTests : AuthorizationSteps\n{\n    private static Dictionary<string, string> GivenRouteClaimsRequirement(FileRoute route, string claimType, string claimValue)\n    {\n        route.AddHeadersToRequest = new()\n        {\n            { \"CustomerId\", \"Claims[CustomerId] > value\" },\n            { \"LocationId\", \"Claims[LocationId] > value\" },\n            { \"UserType\", $\"Claims[{OcelotClaims.OcSub}] > value[0] > |\" },\n            { \"UserId\", $\"Claims[{OcelotClaims.OcSub}] > value[1] > |\" },\n        };\n        route.AddClaimsToRequest = new()\n        {\n            { \"CustomerId\", \"Claims[CustomerId] > value\" },\n            { \"UserType\", $\"Claims[{OcelotClaims.OcSub}] > value[0] > |\" },\n            { \"UserId\", $\"Claims[{OcelotClaims.OcSub}] > value[1] > |\" },\n        };\n        var claims = new Dictionary<string, string>()\n        {\n            {\"CustomerId\", \"111\"},\n            {\"LocationId\", \"222\"},\n            {\"UserType\", \"registered\"},\n        };\n        route.RouteClaimsRequirement = new(claims) // require all custom claims\n        {\n            [claimType] = claimValue, // but require exact claim with the scope after claims-to-claims transformation\n        };\n        return claims;\n    }\n\n    [Fact]\n    [Trait(\"Commit\", \"3285be3\")] // https://github.com/ThreeMammals/Ocelot/commit/3285be3\n    [Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\n    public void Should_return_200_OK_authorizing_route()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        var configuration = GivenConfiguration(route);\n        var claims = GivenRouteClaimsRequirement(route, \"UserType\", OcelotScopes.OcAdmin);\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty<string>(), Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Commit\", \"b8951c4\")] // https://github.com/ThreeMammals/Ocelot/commit/b8951c4\n    [Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\n    public void Should_return_403_Forbidden_authorizing_route()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        var configuration = GivenConfiguration(route);\n        var claims = GivenRouteClaimsRequirement(route, \"UserType\", OcelotScopes.OcAdmin);\n        route.AddClaimsToRequest.Remove(\"UserType\"); // given I don't transform UserType claim\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty<string>(), Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden))\n            .And(x => ThenTheResponseBodyShouldBeEmpty())\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"100\")] // https://github.com/ThreeMammals/Ocelot/issues/100\n    [Trait(\"PR\", \"104\")] // https://github.com/ThreeMammals/Ocelot/pull/104\n    [Trait(\"Release\", \"1.4.5\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5\n    public async Task Should_return_200_OK_using_identity_server_with_allowed_scope()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"api\", \"api.readOnly\", \"openid\", \"offline_access\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        var configuration = GivenConfiguration(route);\n        await GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\");\n\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n\n        await GivenIHaveAToken(scope: \"api.readOnly\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n        await ThenTheResponseBodyShouldBeAsync(\"Hello from Laura\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"100\")] // https://github.com/ThreeMammals/Ocelot/issues/100\n    [Trait(\"PR\", \"104\")] // https://github.com/ThreeMammals/Ocelot/pull/104\n    [Trait(\"Release\", \"1.4.5\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5\n    public void Should_return_403_Forbidden_using_identity_server_with_scope_not_allowed()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"api\", \"openid\", \"offline_access\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        var configuration = GivenConfiguration(route);\n        var testName = TestName();\n        var allScopes = allowedScopes.Append(\"api.readOnly\").ToArray();\n        this.Given(x => GivenThereIsExternalJwtSigningService(allScopes, Xunit.TestContext.Current.CancellationToken))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => GivenIHaveATokenWithScope(\"api.readOnly\", testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden))\n            .BDDfy();\n    }\n\n    /// <summary>\n    /// In ASP.NET Core, the <see cref=\"Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider\"/>  (used for <c>appsettings.json</c> and similar) does not behave like a plain <see cref=\"Dictionary{K,V}\"/>.\n    /// It applies normalization rules to keys when loading configuration.\n    /// That's why keys starting with \"http://\" or \"https://\" don't deserialize as you expect.\n    /// </summary>\n    /// <remarks>AI search:\n    /// C# ASP.NET JsonConfigurationProvider Keys with \"http://\" prefix are not deserialized into dictionary.</remarks>\n    [Fact(DisplayName = \"TODO \" + nameof(Should_fix_issue_240))]\n    [Trait(\"Bug\", \"240\")] // https://github.com/ThreeMammals/Ocelot/issues/240\n    [Trait(\"PR\", \"243\")] // https://github.com/ThreeMammals/Ocelot/pull/243\n    [Trait(\"Release\", \"3.1.6\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.6\n    public void Should_fix_issue_240()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        var configuration = GivenConfiguration(route);\n        route.RouteClaimsRequirement = new() // TODO this is dictionary which doesn't support multiple keys of the same value\n        {\n            { ClaimTypes.Role, \"User\"}, // TODO Such a claim types in a form of URL (aka http://*) are not supported by JsonConfigurationProvider\n            { nameof(ClaimTypes.Role), \"User\"}, // this key is Ok because it is not an URL containing proto delimiter aka '://'\n        };\n        var claims = new List<KeyValuePair<string, string>>()\n        {\n            new(nameof(ClaimTypes.Role), \"AdminUser\"),\n            new(nameof(ClaimTypes.Role), \"User\"),\n        };\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty<string>(), Xunit.TestContext.Current.CancellationToken))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\n    [Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\n    [Trait(\"Release\", \"24.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\n    public async Task Should_return_200_OK_with_global_allowed_scopes()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port);\n        route.AuthenticationOptions.AuthenticationProviderKeys = []; // no route auth!\n        var configuration = GivenConfiguration(route);\n        string[] globalScopes = [\"api\", \"apiGlobal\"];\n        configuration.GlobalConfiguration = GivenGlobalAuthConfiguration(allowedScopes: globalScopes);\n\n        GivenThereIsAConfiguration(configuration);\n        await GivenThereIsExternalJwtSigningService(globalScopes, Xunit.TestContext.Current.CancellationToken);\n        GivenThereIsAServiceRunningOn(port);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenIHaveAToken(scope: \"apiGlobal\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBeOK();\n        await ThenTheResponseBodyAsync();\n    }\n\n    #region PR 1478\n    [Fact]\n    [Trait(\"Bug\", \"913\")] // https://github.com/ThreeMammals/Ocelot/issues/913\n    [Trait(\"PR\", \"1478\")] // https://github.com/ThreeMammals/Ocelot/pull/1478\n    [Trait(\"Release\", \"24.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0\n    public async Task Should_return_200_OK_with_space_separated_scope_match()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, scopes: [\"api\", \"api.read\", \"api.write\"]);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        await GivenThereIsExternalJwtSigningService([\"api.read\", \"openid\", \"offline_access\"], Xunit.TestContext.Current.CancellationToken);\n        GivenThereIsAServiceRunningOn(port);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenIHaveATokenWithScope(\"api.read openid offline_access\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBeOK();\n        await ThenTheResponseBodyAsync();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    [Trait(\"Release\", \"24.1.0\")]\n    public async Task Should_return_403_Forbidden_with_space_separated_scope_no_match()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenAuthRoute(port, scopes: [\"admin\", \"superuser\"]);\n        var configuration = GivenConfiguration(route);\n        await GivenThereIsExternalJwtSigningService([\"api.read\", \"api.write\", \"openid\"], Xunit.TestContext.Current.CancellationToken);\n        GivenThereIsAServiceRunningOn(port);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithJwtBearerAuthentication);\n        await GivenIHaveATokenWithScope(\"api.read api.write openid\");\n        GivenIHaveAddedATokenToMyRequest();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden);\n    }\n    #endregion PR 1478\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Caching/CachingTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing System.Text;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Ocelot.AcceptanceTests.Caching;\n\npublic sealed class CachingTests : Steps\n{\n    private const string HelloTomContent = \"Hello from Tom\";\n    private const string HelloLauraContent = \"Hello from Laura\";\n    private int _counter = 0;\n\n    public CachingTests()\n    {\n    }\n\n    [Fact]\n    public void Should_return_cached_response()\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 100,\n        };\n        var configuration = GivenFileConfiguration(port, options);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, null, null))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .And(x => ThenTheContentLengthIs(HelloLauraContent.Length))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_cached_response_with_expires_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 100,\n        };\n        var configuration = GivenFileConfiguration(port, options);\n        var headerExpires = \"Expires\";\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, headerExpires, \"-1\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .And(x => ThenTheContentLengthIs(HelloLauraContent.Length))\n            .And(x => ThenTheResponseContentHeaderIs(headerExpires, \"-1\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_return_cached_response_as_ttl_expires()\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 1,\n        };\n        var configuration = GivenFileConfiguration(port, options);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, null, null))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null))\n            .And(x => GivenTheCacheExpires())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloTomContent))\n            .BDDfy();\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void Should_return_different_cached_response_when_request_body_changes_and_EnableContentHashing_is_true(bool asGlobalConfig)\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 100,\n            EnableContentHashing = true,\n        };\n        var (testBody1String, testBody2String) = TestBodiesFactory();\n        var configuration = GivenFileConfiguration(port, options, asGlobalConfig, HttpMethods.Post);\n\n        this.Given(x => x.GivenThereIsAnEchoServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody1String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody2String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody2String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody1String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody2String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody2String))\n            .And(x => ThenTheCounterValueShouldBe(2))\n            .BDDfy();\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void Should_return_same_cached_response_when_request_body_changes_and_EnableContentHashing_is_false(bool asGlobalConfig)\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 100,\n        };\n        var (testBody1String, testBody2String) = TestBodiesFactory();\n        var configuration = GivenFileConfiguration(port, options, asGlobalConfig, HttpMethods.Post);\n\n        this.Given(x => x.GivenThereIsAnEchoServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody1String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody2String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody1String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(testBody2String, Encoding.UTF8, \"application/json\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(testBody1String))\n            .And(x => ThenTheCounterValueShouldBe(1))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Issue\", \"1172\")]\n    public void Should_clean_cached_response_by_cache_header_via_new_caching_key()\n    {\n        var port = PortFinder.GetRandomPort();\n        var options = new FileCacheOptions\n        {\n            TtlSeconds = 100,\n            Region = \"europe-central\",\n            Header = \"Authorization\",\n        };\n        var configuration = GivenFileConfiguration(port, options);\n        var headerExpires = \"Expires\";\n\n        // Add to cache\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, headerExpires, options.TtlSeconds))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n\n            // Read from cache\n            .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, headerExpires, options.TtlSeconds / 2))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))\n            .And(x => ThenTheContentLengthIs(HelloLauraContent.Length))\n\n            // Clean cache by the header and cache new content\n            .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, headerExpires, -1))\n            .And(x => GivenIAddAHeader(options.Header, \"123\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(HelloTomContent))\n            .And(x => ThenTheContentLengthIs(HelloTomContent.Length))\n            .BDDfy();\n    }\n\n    private FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions,\n        bool asGlobalConfig = false, params string[] methods)\n    {\n        var route = GivenRoute(port);\n        route.CacheOptions = asGlobalConfig ? new() { TtlSeconds = cacheOptions.TtlSeconds } : cacheOptions;\n        foreach (var m in methods)\n            route.UpstreamHttpMethod.Add(m);\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration = !asGlobalConfig ? null :\n            new()\n            {\n                CacheOptions = new(cacheOptions),\n            };\n        return configuration;\n    }\n\n    private static void GivenTheCacheExpires()\n    {\n        Thread.Sleep(1000);\n    }\n\n    private void GivenTheServiceNowReturns(int port, HttpStatusCode statusCode, string responseBody, string key, object value)\n    {\n        handler.Dispose();\n        GivenThereIsAServiceRunningOn(port, statusCode, responseBody, key, value);\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody, string key, object value)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            if (!string.IsNullOrEmpty(key) && value != null)\n            {\n                context.Response.Headers.Append(key, value.ToString());\n            }\n\n            context.Response.StatusCode = (int)statusCode;\n            return context.Response.WriteAsync(responseBody);\n        });\n    }\n\n    [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Usage\", \"xUnit1013:Public method should be marked as test\", Justification = \"Steps\")]\n    [System.Diagnostics.CodeAnalysis.SuppressMessage(\"CodeQuality\", \"IDE0079:Remove unnecessary suppression\", Justification = \"Steps\")]\n    public void GivenThereIsAnEchoServiceRunningOn(int port)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, async context =>\n        {\n            using var streamReader = new StreamReader(context.Request.Body);\n            var requestBody = await streamReader.ReadToEndAsync();\n            _counter++;\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\n            await context.Response.WriteAsync(requestBody);\n        });\n    }\n\n    private void ThenTheCounterValueShouldBe(int expected)\n    {\n        Assert.Equal(expected, _counter);\n    }\n\n    public static (string TestBody1String, string TestBody2String) TestBodiesFactory()\n    {\n        var testBody1 = new TestBody\n        {\n            Age = 19,\n            Email = \"tom@ocelot.net\",\n            FirstName = \"Tom\",\n            LastName = \"Test\",\n        };\n\n        var testBody1String = JsonSerializer.Serialize(testBody1);\n\n        var testBody2 = new TestBody\n        {\n            Age = 25,\n            Email = \"laura@ocelot.net\",\n            FirstName = \"Laura\",\n            LastName = \"Test\",\n        };\n\n        var testBody2String = JsonSerializer.Serialize(testBody2);\n\n        return (testBody1String, testBody2String);\n    }\n}\n\npublic class TestBody\n{\n    public string FirstName { get; set; }\n    public string LastName { get; set; }\n    public string Email { get; set; }\n    public int Age { get; set; }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/CancelRequestTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.AcceptanceTests;\n\npublic sealed class CancelRequestTests : Steps, IDisposable\n{\n    public CancelRequestTests()\n    {\n    }\n\n    [Fact]\n    public async Task ShouldAbortServiceWork_WhenCancellingTheRequest()\n    {\n        // Arrange\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        var configuration = GivenConfiguration(route);\n        var started = new Notifier(\"service work started notifier\");\n        var stopped = new Notifier(\"service work finished notifier\");\n        GivenThereIsAServiceRunningOn(DownstreamUrl(port), started, stopped);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        // Act: Initialize\n        var getting = WhenIGetUrl(\"/\");\n        var canceling = WhenIWaitForNotification(started).ContinueWith(Cancel);\n        Exception ex = null;\n\n        // Act\n        try\n        {\n            await Task.WhenAll(getting, canceling);\n        }\n        catch (Exception e)\n        {\n            ex = e;\n        }\n\n        // Assert\n        started.NotificationSent.ShouldBeTrue();\n        stopped.NotificationSent.ShouldBeFalse();\n        ex.ShouldNotBeNull().ShouldBeOfType<TaskCanceledException>();\n    }\n\n    private Task Cancel(Task t) => Task.Run(ocelotClient.CancelPendingRequests);\n\n    private void GivenThereIsAServiceRunningOn(string baseUrl, Notifier startedNotifier, Notifier stoppedNotifier)\n    {\n        handler.GivenThereIsAServiceRunningOn(baseUrl, async context =>\n        {\n            startedNotifier.NotificationSent = true;\n            await Task.Delay(SERVICE_WORK_TIME, context.RequestAborted);\n\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\n            await context.Response.WriteAsync(\"OK\");\n            stoppedNotifier.NotificationSent = true;\n        });\n    }\n\n    private const int SERVICE_WORK_TIME = 1_000;\n    private const int WAITING_TIME = 50;\n    private const int MAX_WAITING_TIME = 10_000;\n\n    private static async Task WhenIWaitForNotification(Notifier notifier)\n    {\n        int waitingTime = 0;\n        while (!notifier.NotificationSent)\n        {\n            await Task.Delay(WAITING_TIME);\n            waitingTime += WAITING_TIME;\n            if (waitingTime > MAX_WAITING_TIME)\n            {\n                throw new TimeoutException(notifier.Name + $\" did not sent notification within {MAX_WAITING_TIME / 1000} second(s).\");\n            }\n        }\n    }\n\n    class Notifier\n    {\n        public Notifier(string name) => Name = name;\n        public bool NotificationSent { get; set; }\n        public string Name { get; set; }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs",
    "content": "namespace Ocelot.AcceptanceTests;\r\n\r\npublic class CannotStartOcelotTests : Steps\r\n{\r\n    private static readonly string NL = Environment.NewLine;\r\n\r\n    [Fact]\r\n    public void Should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered_with_dynamic_re_routes()\r\n    {\r\n        var invalidConfig = GivenConfiguration();\r\n        invalidConfig.GlobalConfiguration.ServiceDiscoveryProvider = new()\r\n        {\r\n            Scheme = \"https\",\r\n            Host = \"localhost\",\r\n            Type = nameof(Provider.Consul.Consul),\r\n            Port = 8500,\r\n        };\r\n\r\n        Exception exception = null;\r\n        GivenThereIsAConfiguration(invalidConfig);\r\n        try\r\n        {\r\n            GivenOcelotIsRunning();\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            exception = ex;\r\n        }\r\n\r\n        exception.ShouldNotBeNull();\r\n        exception.Message.ShouldBe($\"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?{NL}\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/laura\", \"/\");\r\n        var invalidConfig = GivenConfiguration(route);\r\n        invalidConfig.GlobalConfiguration.ServiceDiscoveryProvider = new()\r\n        {\r\n            Scheme = \"https\",\r\n            Host = \"localhost\",\r\n            Type = nameof(Provider.Consul.Consul),\r\n            Port = 8500,\r\n        };\r\n\r\n        Exception exception = null;\r\n        GivenThereIsAConfiguration(invalidConfig);\r\n        try\r\n        {\r\n            GivenOcelotIsRunning();\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            exception = ex;\r\n        }\r\n\r\n        exception.ShouldNotBeNull();\r\n        exception.Message.ShouldBe($\"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?{NL}\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/laura\", \"/\");\r\n        var invalidConfig = GivenConfiguration(route);\r\n        invalidConfig.GlobalConfiguration.QoSOptions = new()\r\n        {\r\n            TimeoutValue = 1,\r\n            ExceptionsAllowedBeforeBreaking = 1,\r\n        };\r\n\r\n        Exception exception = null;\r\n        GivenThereIsAConfiguration(invalidConfig);\r\n        try\r\n        {\r\n            GivenOcelotIsRunning();\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            exception = ex;\r\n        }\r\n\r\n        exception.ShouldNotBeNull();\r\n        exception.Message.ShouldBe($\"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?{NL}\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_for_re_route()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/laura\", \"/\");\r\n        route.QoSOptions = new()\r\n        {\r\n            TimeoutValue = 1,\r\n            ExceptionsAllowedBeforeBreaking = 1,\r\n        };\r\n        var invalidConfig = GivenConfiguration(route);\r\n\r\n        Exception exception = null;\r\n        GivenThereIsAConfiguration(invalidConfig);\r\n        try\r\n        {\r\n            GivenOcelotIsRunning();\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            exception = ex;\r\n        }\r\n\r\n        exception.ShouldNotBeNull();\r\n        exception.Message.ShouldBe($\"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?{NL}\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_throw_exception_if_cannot_start()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"api\", \"test\");\r\n        var invalidConfig = GivenConfiguration(route);\r\n        Exception exception = null;\r\n        GivenThereIsAConfiguration(invalidConfig);\r\n        try\r\n        {\r\n            GivenOcelotIsRunning();\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            exception = ex;\r\n        }\r\n\r\n        exception.ShouldNotBeNull();\r\n        exception.Message.ShouldBe($\"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Downstream Path Template test doesnt start with forward slash{NL}FileValidationFailedError: Upstream Path Template api doesnt start with forward slash{NL}\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class CaseSensitiveRoutingTests : Steps\r\n{\r\n    public CaseSensitiveRoutingTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_global_ignore_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/products/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_route_ignore_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/products/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                        RouteIsCaseSensitive = false,\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_404_when_route_respect_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/products/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                        RouteIsCaseSensitive = true,\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_route_respect_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/PRODUCTS/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                        RouteIsCaseSensitive = true,\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_404_when_global_respect_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/products/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                        RouteIsCaseSensitive = true,\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_global_respect_case_sensitivity_set()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = new FileConfiguration\r\n        {\r\n            Routes = new List<FileRoute>\r\n                {\r\n                    new()\r\n                    {\r\n                        DownstreamPathTemplate = \"/api/products/{productId}\",\r\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                        {\r\n                            new()\r\n                            {\r\n                                Host = \"localhost\",\r\n                                Port = port,\r\n                            },\r\n                        },\r\n                        DownstreamScheme = \"http\",\r\n                        UpstreamPathTemplate = \"/PRODUCTS/{productId}\",\r\n                        UpstreamHttpMethod = [\"Get\"],\r\n                        RouteIsCaseSensitive = true,\r\n                    },\r\n                },\r\n        };\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", 200, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/PRODUCTS/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\r\n        {\r\n            context.Response.StatusCode = statusCode;\r\n            await context.Response.WriteAsync(responseBody);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ConcurrentSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.AcceptanceTests.LoadBalancer;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.LoadBalancer;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing System.Runtime.CompilerServices;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests;\n\npublic class ConcurrentSteps : Steps\n{\n    protected Task[] _tasks;\n    protected ConcurrentDictionary<int, HttpResponseMessage> _responses;\n    protected volatile int[] _counters;\n\n    public ConcurrentSteps()\n    {\n        _tasks = Array.Empty<Task>();\n        _responses = new();\n        _counters = Array.Empty<int>();\n    }\n\n    public override void Dispose()\n    {\n        foreach (var response in _responses.Values)\n        {\n            response?.Dispose();\n        }\n\n        foreach (var task in _tasks)\n        {\n            task?.Dispose();\n        }\n\n        base.Dispose();\n        GC.SuppressFinalize(this);\n    }\n\n    protected void GivenServiceInstanceIsRunning(string url, string response)\n        => GivenServiceInstanceIsRunning(url, response, HttpStatusCode.OK);\n\n    protected void GivenServiceInstanceIsRunning(string url, string response, HttpStatusCode statusCode)\n    {\n        _counters = new int[1]; // single counter\n        GivenServiceIsRunning(url, response, 0, statusCode);\n        _counters[0] = 0;\n    }\n\n    protected void GivenThereIsAServiceRunningOn(string url, string basePath, string responseBody)\n    {\n        handler.GivenThereIsAServiceRunningOn(url, basePath, MapGet(basePath, responseBody));\n    }\n\n    protected void GivenMultipleServiceInstancesAreRunning(string[] urls, [CallerMemberName] string serviceName = null)\n    {\n        serviceName ??= new Uri(urls[0]).Host;\n        string[] responses = urls.Select(u => $\"{serviceName}|url({u})\").ToArray();\n        GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK);\n    }\n\n    protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses)\n        => GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK);\n\n    protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses, HttpStatusCode statusCode)\n    {\n        Debug.Assert(urls.Length == responses.Length, \"Length mismatch!\");\n        _counters = new int[urls.Length]; // multiple counters\n        for (int i = 0; i < urls.Length; i++)\n        {\n            GivenServiceIsRunning(urls[i], responses[i], i, statusCode);\n            _counters[i] = 0;\n        }\n    }\n    protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses, HttpStatusCode[] codes)\n    {\n        Debug.Assert(urls.Length == responses.Length, \"Length mismatch!\");\n        Debug.Assert(urls.Length == codes.Length, \"Length mismatch!\");\n        Debug.Assert(responses.Length == codes.Length, \"Length mismatch!\");\n        _counters = new int[urls.Length]; // multiple counters\n        for (int i = 0; i < urls.Length; i++)\n        {\n            GivenServiceIsRunning(urls[i], responses[i], i, codes[i]);\n            _counters[i] = 0;\n        }\n    }\n\n    private void GivenServiceIsRunning(string url, string response)\n        => GivenServiceIsRunning(url, response, 0, HttpStatusCode.OK);\n    private void GivenServiceIsRunning(string url, string response, int index)\n        => GivenServiceIsRunning(url, response, index, HttpStatusCode.OK);\n\n    private void GivenServiceIsRunning(string url, string response, int index, HttpStatusCode successCode)\n    {\n        response ??= successCode.ToString();\n        handler.GivenThereIsAServiceRunningOn(url, MapGet(index, response, successCode));\n    }\n\n    protected static RequestDelegate MapGet(string path, string responseBody) => MapGet(path, responseBody, HttpStatusCode.OK);\n    protected static RequestDelegate MapGet(string path, string responseBody, HttpStatusCode statusCode) => async context =>\n    {\n        var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value)\n            ? context.Request.PathBase.Value\n            : context.Request.Path.Value;\n        bool isMatch = downstreamPath == path;\n        context.Response.StatusCode = (int)(isMatch ? statusCode : HttpStatusCode.NotFound);\n        await context.Response.WriteAsync(isMatch ? responseBody : \"Not Found\");\n    };\n\n    public static class HeaderNames\n    {\n        public const string ServiceIndex = nameof(LeaseEventArgs.ServiceIndex);\n        public const string Host = nameof(Uri.Host);\n        public const string Port = nameof(Uri.Port);\n        public const string Counter = nameof(Counter);\n        public const string Path = nameof(Path);\n    }\n\n    protected RequestDelegate MapGet(int index, string body) => MapGet(index, body, HttpStatusCode.OK);\n    protected RequestDelegate MapGet(int index, string body, HttpStatusCode successCode) => async context =>\n    {\n        // Don't delay during the first service call\n        if (Volatile.Read(ref _counters[index]) > 0)\n        {\n            await Task.Delay(Random.Shared.Next(5, 15)); // emulate integration delay up to 15 milliseconds\n        }\n\n        string responseBody;\n        var request = context.Request;\n        var response = context.Response;\n        try\n        {\n            int count = Interlocked.Increment(ref _counters[index]);\n            responseBody = string.Concat(count, CounterSeparator, body);\n\n            response.StatusCode = (int)successCode;\n            response.Headers.Append(HeaderNames.ServiceIndex, new StringValues(index.ToString()));\n            response.Headers.Append(HeaderNames.Host, new StringValues(request.Host.Host));\n            response.Headers.Append(HeaderNames.Port, new StringValues(request.Host.Port.ToString()));\n            response.Headers.Append(HeaderNames.Counter, new StringValues(count.ToString()));\n            response.Headers.Append(HeaderNames.Path, new StringValues(request.Path + request.QueryString));\n            await response.WriteAsync(responseBody);\n        }\n        catch (Exception exception)\n        {\n            responseBody = string.Concat(1, CounterSeparator, exception.StackTrace);\n            response.StatusCode = (int)HttpStatusCode.InternalServerError;\n            await response.WriteAsync(responseBody);\n        }\n    };\n\n    public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(string url, int times)\n        => RunParallelRequests(times, (i) => url);\n\n    public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(int times, params string[] urls)\n        => RunParallelRequests(times, (i) => urls[i % urls.Length]);\n\n    protected Task[] RunParallelRequests(int times, Func<int, string> urlFunc)\n    {\n        _tasks = new Task[times];\n        _responses = new(times, times);\n        for (var i = 0; i < times; i++)\n        {\n            var url = urlFunc(i);\n            _tasks[i] = GetParallelResponse(url, i);\n            _responses[i] = null;\n        }\n\n        Task.WaitAll(_tasks);\n        return _tasks;\n    }\n\n    protected const string CounterSeparator = \"^:^\";\n    private async Task GetParallelResponse(string url, int threadIndex)\n    {\n        var response = await ocelotClient.GetAsync(url);\n        var content = await response.Content.ReadAsStringAsync();\n        var counterString = content.Contains(CounterSeparator)\n            ? content.Split(CounterSeparator)[0] // let the first fragment is counter value\n            : \"0\";\n        int count = int.Parse(counterString);\n        if (content.IsNotEmpty()) count.ShouldBeGreaterThan(0);\n        _responses[threadIndex] = response;\n    }\n\n    public void ThenAllStatusCodesShouldBe(HttpStatusCode expected)\n        => _responses.ShouldAllBe(response => response.Value.StatusCode == expected);\n\n    public void ThenAllResponseBodiesShouldBe(string expectedBody)\n    {\n        foreach (var r in _responses)\n        {\n            var content = r.Value.Content.ReadAsStringAsync().Result;\n            content = content?.Contains(CounterSeparator) == true\n                ? content.Split(CounterSeparator)[1] // remove counter for body comparison\n                : \"0\";\n\n            content.ShouldBe(expectedBody);\n        }\n    }\n    public void ThenAllResponseBodiesShouldBe(int[] ports, string[] expected)\n    {\n        foreach (var r in _responses)\n        {\n            var response = r.Value;\n            var portHeader = response.Headers.GetValues(\"Port\").Csv();\n            int port = int.Parse(portHeader);\n            int i = Array.IndexOf(ports, port);\n            var expectedBody = expected[i];\n            var content = response.Content.ReadAsStringAsync().Result;\n            content = content?.Contains(CounterSeparator) == true\n                ? content.Split(CounterSeparator)[1] // remove counter for body comparison\n                : \"0\";\n            content.ShouldBe(expectedBody);\n        }\n    }\n\n    protected string CalledTimesMessage()\n        => $\"All values are [{_counters.Csv()}]\";\n\n    public void ThenAllServicesShouldHaveBeenCalledTimes(int expected)\n        => _counters.Sum().ShouldBe(expected, CalledTimesMessage());\n\n    public void ThenServiceShouldHaveBeenCalledTimes(int index, int expected)\n        => _counters[index].ShouldBe(expected, CalledTimesMessage());\n\n    public void ThenServicesShouldHaveBeenCalledTimes(params int[] expected)\n    {\n        for (int i = 0; i < expected.Length; i++)\n        {\n            _counters[i].ShouldBe(expected[i], CalledTimesMessage());\n        }\n    }\n\n    public static int Bottom(int totalRequests, int totalServices)\n        => totalRequests / totalServices;\n    public static int Top(int totalRequests, int totalServices)\n    {\n        int bottom = Bottom(totalRequests, totalServices);\n        return totalRequests - (bottom * totalServices) + bottom;\n    }\n\n    public void ThenAllServicesCalledRealisticAmountOfTimes(int bottom, int top)\n    {\n        var customMessage = new StringBuilder()\n            .AppendLine($\"{nameof(bottom)}: {bottom}\")\n            .AppendLine($\"    {nameof(top)}: {top}\")\n            .AppendLine($\"    All values are [{_counters.Csv()}]\")\n            .ToString();\n        int sum = 0, totalSum = _counters.Sum();\n\n        // Last offline services cannot be called at all, thus don't assert zero counters\n        for (int i = 0; i < _counters.Length && sum < totalSum; i++)\n        {\n            int actual = _counters[i];\n            actual.ShouldBeInRange(bottom, top, customMessage);\n            sum += actual;\n        }\n    }\n\n    public void ThenAllServicesCalledOptimisticAmountOfTimes(ILoadBalancerAnalyzer analyzer)\n    {\n        if (analyzer == null) return;\n        int bottom = analyzer.BottomOfConnections(),\n            top = analyzer.TopOfConnections();\n        bottom = Math.Min(bottom, _counters.Min());\n        ThenAllServicesCalledRealisticAmountOfTimes(bottom, top); // with unstable checkings\n    }\n\n    public void ThenServiceCountersShouldMatchLeasingCounters(ILoadBalancerAnalyzer analyzer, int[] ports, int totalRequests)\n    {\n        if (analyzer == null || ports == null)\n            return;\n\n        analyzer.ShouldNotBeNull().Analyze();\n        analyzer.Events.Count.ShouldBe(totalRequests, $\"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}\");\n\n        var leasingCounters = analyzer?.GetHostCounters() ?? new();\n        var sortedLeasingCountersByPort = ports.Select(port => leasingCounters.FirstOrDefault(kv => kv.Key.DownstreamPort == port).Value).ToArray();\n        for (int i = 0; i < ports.Length; i++)\n        {\n            var host = leasingCounters.Keys.FirstOrDefault(k => k.DownstreamPort == ports[i]);\n\n            // Leasing info/counters can be absent because of offline service instance with exact port in unstable scenario\n            if (host != null)\n            {\n                var customMessage = new StringBuilder()\n                    .AppendLine($\"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}\")\n                    .AppendLine($\"    Port: {ports[i]}\")\n                    .AppendLine($\"    Host: {host}\")\n                    .AppendLine($\"    Service counters: [{_counters.Csv()}]\")\n                    .AppendLine($\"    Leasing counters: [{sortedLeasingCountersByPort.Csv()}]\") // should have order of _counters\n                    .ToString();\n                int counter1 = _counters[i];\n                int counter2 = leasingCounters[host];\n                counter1.ShouldBe(counter2, customMessage);\n            }\n        }\n    }\n\n    protected IEnumerable<string> ThenAllResponsesHeaderExists(string key)\n    {\n        foreach (var kv in _responses)\n        {\n            var response = kv.Value.ShouldNotBeNull();\n            response.Headers.Contains(key).ShouldBeTrue();\n            var header = response.Headers.GetValues(key);\n            yield return string.Join(';', header);\n        }\n    }\n\n    protected virtual string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? GetType().Name;\n    protected virtual string ServiceNamespace() => GetType().Namespace;\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Configuration/ConfigurationInConsulTests.cs",
    "content": "using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Consul;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.Configuration;\n\npublic sealed class ConfigurationInConsulTests : Steps\n{\n    private FileConfiguration _config;\n    private readonly List<ServiceEntry> _consulServices;\n\n    public ConfigurationInConsulTests()\n    {\n        _consulServices = new List<ServiceEntry>();\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url_when_using_jsonserialized_cache()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(servicePort);\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = Uri.UriSchemeHttp,\n            Host = \"localhost\",\n            Port = consulPort,\n        };\n        this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty))\n            .And(x => GivenThereIsAServiceRunningOn(servicePort, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    private void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()\n    {\n        static void WithConsulToStoreConfigAndJsonSerializedCache(IServiceCollection services) => services\n            .AddOcelot()\n            .AddConsul()\n            .AddConfigStoredInConsul();\n        GivenOcelotIsRunning(WithConsulToStoreConfigAndJsonSerializedCache);\n    }\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int consulPort, string serviceName)\n    {\n        handler.GivenThereIsAServiceRunningOn(consulPort, async context =>\n        {\n            if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.InvariantCultureIgnoreCase) &&\n                context.Request.Path.Value == \"/v1/kv/InternalConfiguration\")\n            {\n                var json = JsonConvert.SerializeObject(_config);\n                var bytes = Encoding.UTF8.GetBytes(json);\n                var base64 = Convert.ToBase64String(bytes);\n                var kvp = new FakeConsulGetResponse(base64);\n\n                // await context.Response.WriteJsonAsync(new[] { kvp });\n            }\n            else if (context.Request.Method.Equals(HttpMethods.Put, StringComparison.InvariantCultureIgnoreCase) &&\n                context.Request.Path.Value == \"/v1/kv/InternalConfiguration\")\n            {\n                try\n                {\n                    var reader = new StreamReader(context.Request.Body);\n\n                    // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.\n                    // var json = reader.ReadToEnd();                                            \n                    var json = await reader.ReadToEndAsync();\n                    _config = JsonConvert.DeserializeObject<FileConfiguration>(json);\n                    var response = JsonConvert.SerializeObject(true);\n                    await context.Response.WriteAsync(response);\n                }\n                catch (Exception e)\n                {\n                    Console.WriteLine(e);\n                    throw;\n                }\n            }\n            else if (context.Request.Path.Value == $\"/v1/health/service/{serviceName}\")\n            {\n                //await context.Response.WriteJsonAsync(_consulServices);\n            }\n        });\n    }\n\n    public class FakeConsulGetResponse\n    {\n        public FakeConsulGetResponse(string value) => Value = value;\n\n        public int CreateIndex => 100;\n        public int ModifyIndex => 200;\n        public int LockIndex => 200;\n        public string Key => \"InternalConfiguration\";\n        public int Flags => 0;\n        public string Value { get; }\n        public string Session => \"adf4238a-882b-9ddc-4a9d-5b6758e4159e\";\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Configuration/ConfigurationMergeTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Configuration;\n\npublic sealed class ConfigurationMergeTests : Steps\n{\n    private readonly FileConfiguration _initialGlobalConfig;\n    private readonly string _globalConfigFileName;\n\n    public ConfigurationMergeTests() : base()\n    {\n        _initialGlobalConfig = new();\n        _globalConfigFileName = $\"{TestID}-{ConfigurationBuilderExtensions.GlobalConfigFile}\";\n        Files.Add(_globalConfigFileName);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1216\")]\n    [Trait(\"Feat\", \"1227\")]\n    [InlineData(MergeOcelotJson.ToFile, true)]\n    [InlineData(MergeOcelotJson.ToMemory, false)]\n    public void ShouldRunWithGlobalConfigMerged_WithExplicitGlobalConfigFileParameter(MergeOcelotJson where, bool fileExist)\n    {\n        Arrange();\n\n        // Act\n        GivenOcelotIsRunning((context, config) => config\n            .SetBasePath(context.HostingEnvironment.ContentRootPath)\n            .AddOcelot(_initialGlobalConfig, context.HostingEnvironment, where, ocelotConfigFileName, _globalConfigFileName, null, false, false));\n\n        // Assert\n        TheOcelotPrimaryConfigFileExists(fileExist);\n        ThenGlobalConfigurationHasBeenMerged();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2084\")]\n    [InlineData(MergeOcelotJson.ToFile, true)]\n    [InlineData(MergeOcelotJson.ToMemory, false)]\n    public void ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter(MergeOcelotJson where, bool fileExist)\n    {\n        Arrange();\n        var globalConfig = _initialGlobalConfig;\n        globalConfig.Routes.Clear();\n        var routeAConfig = GivenConfiguration(GetRoute(\"A\"));\n        var routeBConfig = GivenConfiguration(GetRoute(\"B\"));\n        var environmentConfig = GivenConfiguration(GetRoute(\"Env\"));\n        environmentConfig.GlobalConfiguration = null;\n        var folder = \"GatewayConfiguration-\" + TestID;\n        Folders.Add(Directory.CreateDirectory(folder).FullName);\n        var globalPath = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile);\n        var routeAPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, \"A\"));\n        var routeBPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, \"B\"));\n        var environmentPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, \"Env\"));\n        GivenThereIsAConfiguration(globalConfig, globalPath);\n        GivenThereIsAConfiguration(routeAConfig, routeAPath);\n        GivenThereIsAConfiguration(routeBConfig, routeBPath);\n        GivenThereIsAConfiguration(environmentConfig, environmentPath);\n\n        // Act\n        GivenOcelotIsRunning(\n            (context, config) => config\n                .SetBasePath(context.HostingEnvironment.ContentRootPath)\n                .AddOcelot(folder, context.HostingEnvironment, where) // overloaded version from the user's scenario\n                .AddJsonFile(environmentPath),\n            null, null, null, host => host.UseEnvironment(\"Env\"), null, null);\n\n        // Assert\n        TheOcelotPrimaryConfigFileExists(false);\n        ThenGlobalConfigurationHasBeenMerged();\n\n        var actualLocation = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile);\n        File.Exists(actualLocation).ShouldBe(fileExist);\n\n        var repository = OcelotServices.GetService<IInternalConfigurationRepository>().ShouldNotBeNull();\n        var response = repository.Get().ShouldNotBeNull();\n        response.IsError.ShouldBeFalse();\n        var internalConfig = response.Data.ShouldNotBeNull();\n\n        // Assert Arrange() setup\n        internalConfig.RequestId.ShouldBe(nameof(ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter));\n        internalConfig.ServiceProviderConfiguration.ConfigurationKey.ShouldBe(nameof(ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter));\n    }\n\n    private void Arrange([CallerMemberName] string testName = null)\n    {\n        _initialGlobalConfig.GlobalConfiguration.RequestIdKey = testName;\n        _initialGlobalConfig.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = testName;\n    }\n\n    private void TheOcelotPrimaryConfigFileExists(bool expected)\n        => File.Exists(ocelotConfigFileName).ShouldBe(expected);\n\n    private void ThenGlobalConfigurationHasBeenMerged([CallerMemberName] string testName = null)\n    {\n        var config = OcelotServices.GetService<IConfiguration>().ShouldNotBeNull();\n        var actual = config[\"GlobalConfiguration:RequestIdKey\"];\n        actual.ShouldNotBeNull().ShouldBe(testName);\n        actual = config[\"GlobalConfiguration:ServiceDiscoveryProvider:ConfigurationKey\"];\n        actual.ShouldNotBeNull().ShouldBe(testName);\n    }\n\n    private static FileRoute GetRoute(string suffix, [CallerMemberName] string testName = null) => new()\n    {\n        DownstreamScheme = nameof(FileRoute.DownstreamScheme) + suffix,\n        DownstreamPathTemplate = \"/\" + suffix,\n        Key = testName + suffix,\n        UpstreamPathTemplate = \"/\" + suffix,\n        UpstreamHttpMethod = [ nameof(FileRoute.UpstreamHttpMethod) + suffix ],\n        DownstreamHostAndPorts = new()\n        {\n            new(nameof(FileHostAndPort.Host) + suffix, 80),\n        },\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Configuration/ConfigurationReloadTests.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\n\nnamespace Ocelot.AcceptanceTests.Configuration;\n\npublic sealed class ConfigurationReloadTests : Steps\n{\n    private readonly FileConfiguration _initialConfig;\n    private readonly FileConfiguration _anotherConfig;\n    private IOcelotConfigurationChangeTokenSource _changeToken;\n\n    public ConfigurationReloadTests()\n    {\n        _initialConfig = new FileConfiguration\n        {\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                RequestIdKey = \"initialKey\",\n            },\n        };\n        _anotherConfig = new FileConfiguration\n        {\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                RequestIdKey = \"someOtherKey\",\n            },\n        };\n    }\n\n    [Fact]\n    public void Should_reload_config_on_change()\n    {\n        this.Given(x => GivenThereIsAConfiguration(_initialConfig))\n            .And(x => GivenOcelotIsRunningReloadingConfig(true))\n            .And(x => GivenThereIsAConfiguration(_anotherConfig))\n            .And(x => ThenConfigShouldBeWithTimeout(_anotherConfig, 10000))\n            .BDDfy();\n    }\n\n    private async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs)\n    {\n        var result = await Wait.For(timeoutMs).UntilAsync(async () =>\n        {\n            var internalConfigCreator = OcelotServices.GetService<IInternalConfigurationCreator>();\n            var internalConfigRepo = OcelotServices.GetService<IInternalConfigurationRepository>();\n            var internalConfig = internalConfigRepo.Get();\n            var config = await internalConfigCreator.Create(fileConfig);\n            return internalConfig.Data.RequestId == config.Data.RequestId;\n        });\n        result.ShouldBe(true);\n    }\n\n    [Fact]\n    public void Should_not_reload_config_on_change()\n    {\n        this.Given(x => GivenThereIsAConfiguration(_initialConfig))\n            .And(x => GivenOcelotIsRunningReloadingConfig(false))\n            .And(x => GivenThereIsAConfiguration(_anotherConfig))\n            .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken))\n            .And(x => ThenConfigShouldBe(_initialConfig))\n            .BDDfy();\n    }\n\n    private async Task ThenConfigShouldBe(FileConfiguration fileConfig)\n    {\n        var internalConfigCreator = OcelotServices.GetService<IInternalConfigurationCreator>();\n        var internalConfigRepo = OcelotServices.GetService<IInternalConfigurationRepository>();\n        var internalConfig = internalConfigRepo.Get();\n        var config = await internalConfigCreator.Create(fileConfig);\n        internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId);\n    }\n\n    [Fact]\n    public void Should_trigger_change_token_on_change()\n    {\n        this.Given(x => GivenThereIsAConfiguration(_initialConfig))\n            .And(x => GivenOcelotIsRunningReloadingConfig(true))\n            .And(x => GivenIHaveAChangeToken())\n            .And(x => GivenThereIsAConfiguration(_anotherConfig))\n            .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken))\n            .Then(x => TheChangeTokenShouldBeActive(true))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_trigger_change_token_with_no_change()\n    {\n        this.Given(x => GivenThereIsAConfiguration(_initialConfig))\n            .And(x => GivenOcelotIsRunningReloadingConfig(false))\n            .And(x => GivenIHaveAChangeToken())\n            .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken)) // Wait for prior activation to expire.\n            .And(x => GivenThereIsAConfiguration(_anotherConfig))\n            .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken))\n            .Then(x => TheChangeTokenShouldBeActive(false))\n            .BDDfy();\n    }\n\n    private const int MillisecondsToWaitForChangeToken = (int)(OcelotConfigurationChangeToken.PollingIntervalSeconds * 1000) - 100;\n\n    private void GivenOcelotIsRunningReloadingConfig(bool shouldReload)\n    {\n        GivenOcelotIsRunning((context, config) => config\n            .SetBasePath(context.HostingEnvironment.ContentRootPath)\n            .AddOcelot(ocelotConfigFileName, false, shouldReload));\n    }\n\n    private void GivenIHaveAChangeToken()\n    {\n        _changeToken = OcelotServices.GetRequiredService<IOcelotConfigurationChangeTokenSource>();\n    }\n\n    private void TheChangeTokenShouldBeActive(bool itShouldBeActive)\n    {\n        _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Configuration/DownstreamHttpVersionTests.cs",
    "content": "using Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Ocelot.Configuration.File;\nusing System.Security.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Configuration;\n\n[Trait(\"PR\", \"1127\")]\n[Trait(\"Feat\", \"1124\")]\npublic sealed class DownstreamHttpVersionTests : Steps\n{\n    [Fact]\n    public void Should_return_response_200_when_using_http_one()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version10);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpProtocols.Http1))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_when_using_http_one_point_one()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version11);\n        route.DangerousAcceptAnyServerCertificateValidator = true;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpProtocols.Http1))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_when_using_http_two_point_zero()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, Uri.UriSchemeHttps, HttpVersion.Version20);\n        route.DangerousAcceptAnyServerCertificateValidator = true;\n        var configuration = GivenConfiguration(route);\n\n        const string expected = \"here is some content\";\n        var httpContent = new StringContent(expected);\n\n        this.Given(x => x.GivenThereIsAServiceUsingHttpsRunningOn(port, \"/\", HttpProtocols.Http2))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\", httpContent))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe(expected))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_502_when_using_http_one_to_talk_to_server_running_http_two()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, Uri.UriSchemeHttps, HttpVersion.Version11);\n        route.DangerousAcceptAnyServerCertificateValidator = true;\n        var configuration = GivenConfiguration(route);\n\n        const string expected = \"here is some content\";\n        var httpContent = new StringContent(expected);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpProtocols.Http2))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\", httpContent))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\n            .BDDfy();\n    }\n\n    //TODO: does this test make any sense?\n    [Fact]\n    public void Should_return_response_200_when_using_http_two_to_talk_to_server_running_http_one_point_one()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version11);\n        route.DangerousAcceptAnyServerCertificateValidator = true;\n        var configuration = GivenConfiguration(route);\n\n        const string expected = \"here is some content\";\n        var httpContent = new StringContent(expected);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpProtocols.Http1))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\", httpContent))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe(expected))\n            .BDDfy();\n    }\n\n    private FileRoute GivenRoute(int port, string scheme, Version httpVersion) => new()\n    {\n        DownstreamPathTemplate = \"/{url}\",\n        DownstreamScheme = scheme ?? Uri.UriSchemeHttp,\n        UpstreamPathTemplate = \"/{url}\",\n        UpstreamHttpMethod = [HttpMethods.Get],\n        DownstreamHostAndPorts = [Localhost(port)],\n        DownstreamHttpMethod = HttpMethods.Get,\n        DownstreamHttpVersion = httpVersion.ToString(),\n    };\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpProtocols protocols)\n    {\n        void options(KestrelServerOptions serverOptions)\n        {\n            serverOptions.Listen(IPAddress.Loopback, port, listenOptions =>\n            {\n                listenOptions.Protocols = protocols;\n            });\n        }\n\n        handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, options, async context =>\n        {\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\n            var reader = new StreamReader(context.Request.Body);\n            var body = await reader.ReadToEndAsync();\n            await context.Response.WriteAsync(body);\n        });\n    }\n\n    private void GivenThereIsAServiceUsingHttpsRunningOn(int port, string basePath, HttpProtocols protocols)\n    {\n        void options(KestrelServerOptions serverOptions)\n        {\n            serverOptions.Listen(IPAddress.Loopback, port, listenOptions =>\n            {\n                listenOptions.UseHttps(\"mycert2.pfx\", \"password\", options =>\n                {\n                    options.SslProtocols = SslProtocols.Tls12;\n                });\n                listenOptions.Protocols = protocols;\n            });\n        }\n\n        handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, options, async context =>\n        {\n            context.Response.StatusCode = 200;\n            var reader = new StreamReader(context.Request.Body);\n            var body = await reader.ReadToEndAsync();\n            await context.Response.WriteAsync(body);\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Configuration/TimeoutTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing System.Diagnostics;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Configuration;\n\n[Trait(\"PR\", \"2073\")] // https://github.com/ThreeMammals/Ocelot/pull/2073\npublic class TimeoutTests : TimeoutTestsBase\n{\n    [Fact]\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    public async Task HasRouteAndGlobalTimeouts_RouteTimeoutShouldTakePrecedenceOverGlobalTimeout()\n    {\n        const int RouteTimeoutSeconds = 2, GlobalTimeoutSeconds = 4;\n        int serviceTimeoutMs = Ms(Math.Max(RouteTimeoutSeconds, GlobalTimeoutSeconds)) + 500; // total 4.5 sec\n        var port = PortFinder.GetRandomPort();\n        var configuration = GivenConfiguration(port, RouteTimeoutSeconds, GlobalTimeoutSeconds); // !!!\n\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        var watcher = await WatchWhenIGetUrlOnTheApiGateway();\n\n        ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), Ms(RouteTimeoutSeconds) + 500); // (2.0, 2.5) s\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler\n        await ThenTheResponseBodyShouldBeAsync(string.Empty);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    public async Task HasGlobalTimeoutOnly_ForAllRoutesGlobalTimeoutShouldTakePrecedenceOverAbsoluteGlobalTimeout()\n    {\n        const int GlobalTimeoutSeconds = 2;\n        int serviceTimeoutMs = Ms(GlobalTimeoutSeconds + 1); // total 3 sec\n        var ports = PortFinder.GetPorts(2);\n        FileRoute route1 = GivenRoute(ports[0], \"/route1\"), route2 = GivenRoute(ports[1], \"/route2\"); // without timeouts\n        var configuration = GivenConfiguration(route1, route2);\n        configuration.GlobalConfiguration.Timeout = GlobalTimeoutSeconds; // !!!\n        GivenThereIsAServiceRunningOn(ports[0], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable\n        GivenThereIsAServiceRunningOn(ports[1], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        var watchers = await Task.WhenAll<Stopwatch>(\n            WatchWhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate),\n            WatchWhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate));\n\n        int globalTimeoutMs = Ms(GlobalTimeoutSeconds);\n        foreach (var watcher in watchers)\n        {\n            ThenTimeoutIsInRange(watcher, globalTimeoutMs, Ms(DownstreamRoute.DefaultTimeoutSeconds)); // (2.0, 90) so assert roughly\n            ThenTimeoutIsInRange(watcher, globalTimeoutMs, globalTimeoutMs + 500); // (2.0, 2.5) so assert precisely\n            ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler\n            await ThenTheResponseBodyShouldBeAsync(string.Empty);\n        }\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1869\")] // https://github.com/ThreeMammals/Ocelot/issues/1869\n    public async Task HasRouteTimeout_ShouldTimeoutAfterRouteTimeout()\n    {\n        const int RouteTimeoutSeconds = 2;\n        int serviceTimeoutMs = Ms(RouteTimeoutSeconds) + 500; // total 2.5 sec\n        var port = PortFinder.GetRandomPort();\n        var configuration = GivenConfiguration(port, RouteTimeoutSeconds); // !!!\n\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 2.5s > 2s -> ServiceUnavailable\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        var watcher = await WatchWhenIGetUrlOnTheApiGateway();\n\n        ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), serviceTimeoutMs);\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler\n        await ThenTheResponseBodyShouldBeAsync(string.Empty);\n    }\n\n    [Collection(nameof(SequentialTests))]\n    public class Sequential : TimeoutTestsBase\n    {\n        [Fact]\n        [Trait(\"PR\", \"2073\")] // https://github.com/ThreeMammals/Ocelot/pull/2073\n        [Trait(\"Feat\", \"1869\")] // https://github.com/ThreeMammals/Ocelot/issues/1869\n        public async Task NoRouteTimeoutAndNoGlobalOne_ShouldTimeoutAfterCustomDefaultTimeout()\n        {\n            try\n            {\n                DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // override original 90s with 3s\n                int serviceTimeoutMs = Ms(DownstreamRoute.LowTimeout) + 500; // total 3.5 sec\n                var port = PortFinder.GetRandomPort();\n                var configuration = GivenConfiguration(port, routeTimeout: null, globalTimeout: null); // !!! no route timeout -> DownstreamRoute.DefaultTimeoutSeconds\n                GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 3.5s > 3s -> ServiceUnavailable\n                GivenThereIsAConfiguration(configuration);\n                GivenOcelotIsRunning();\n\n                var watcher = await WatchWhenIGetUrlOnTheApiGateway();\n\n                ThenTimeoutIsInRange(watcher, Ms(DownstreamRoute.LowTimeout), serviceTimeoutMs);\n                ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 3 secs -> TimeoutException by TimeoutDelegatingHandler\n                await ThenTheResponseBodyShouldBeAsync(string.Empty);\n            }\n            finally\n            {\n                DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout;\n            }\n        }\n    }\n}\n\npublic class TimeoutTestsBase : Steps\n{\n    protected static int Ms(int seconds) => 1000 * seconds;\n\n    protected FileConfiguration GivenConfiguration(int port, int? routeTimeout = null, int? globalTimeout = null)\n    {\n        var route = GivenDefaultRoute(port);\n        route.Timeout = routeTimeout;\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.Timeout = globalTimeout;\n        return configuration;\n    }\n\n    protected virtual void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, int timeout, [CallerMemberName] string response = nameof(TimeoutTests))\n    {\n        async Task MapBodyWithTimeout(HttpContext context)\n        {\n            await Task.Delay(timeout);\n            context.Response.StatusCode = (int)statusCode;\n            await context.Response.WriteAsync(response);\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapBodyWithTimeout);\n    }\n\n    protected async Task<Stopwatch> WatchWhenIGetUrlOnTheApiGateway(string upstream = null)\n    {\n        var watcher = Stopwatch.StartNew();\n        await WhenIGetUrlOnTheApiGateway(upstream ?? \"/\");\n        watcher.Stop();\n        return watcher;\n    }\n\n    protected static void ThenTimeoutIsInRange(Stopwatch watcher, int lowDurationMs, int highDurationMs)\n    {\n        var expectedLowDuration = TimeSpan.FromMilliseconds(lowDurationMs);\n        var expectedHighDuration = TimeSpan.FromMilliseconds(highDurationMs);\n        watcher.Elapsed.ShouldBeGreaterThan(expectedLowDuration);\n        watcher.Elapsed.ShouldBeLessThan(expectedHighDuration);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ContentTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Configuration.File;\r\nusing System.Diagnostics;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class ContentTests : Steps\r\n{\r\n    private string _contentType;\r\n    private long? _contentLength;\r\n    private long _memoryUsageAfterCallToService;\r\n    private bool _contentTypeHeaderExists;\r\n\r\n    public ContentTests() : base()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_Not_add_content_type_or_content_length_headers()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(port);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .And(x => ThenTheContentTypeShouldBeEmpty())\r\n            .And(x => ThenTheContentLengthShouldBeZero())\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_content_type_and_content_length_headers()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(port, HttpMethods.Post);\r\n        var contentType = \"application/json\";\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.Created, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\", contentType))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created))\r\n            .And(x => ThenTheContentTypeIsIs(contentType))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_default_content_type_header()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(port, HttpMethods.Post);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.Created, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created))\r\n            .And(x => ThenTheContentTypeIsIs(\"text/plain; charset=utf-8\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"1824\")]\r\n    [Trait(\"Issues\", \"356 695 1924\")]\r\n    public void Should_Not_increase_memory_usage_When_downloading_large_file()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(port);\r\n        var dummyDatFilePath = GenerateDummyDatFile(100);\r\n        this.Given(x => x.GivenThereIsAServiceWithPayloadRunningOn(port, \"/\", dummyDatFilePath))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .Then(x => x.ThenMemoryUsageShouldNotIncrease())\r\n            .BDDfy();\r\n    }\r\n\r\n    private void ThenMemoryUsageShouldNotIncrease()\r\n    {\r\n        var currentMemoryUsage = Process.GetCurrentProcess().WorkingSet64;\r\n        var tolerance = currentMemoryUsage - (10 * 1024 * 1024L);\r\n        Assert.InRange(_memoryUsageAfterCallToService, currentMemoryUsage - tolerance, currentMemoryUsage + tolerance);\r\n    }\r\n\r\n    private void ThenTheContentTypeIsIs(string expected)\r\n    {\r\n        _contentType.ShouldBe(expected);\r\n    }\r\n\r\n    private void ThenTheContentLengthShouldBeZero()\r\n    {\r\n        _contentLength.ShouldBeNull();\r\n    }\r\n\r\n    private void ThenTheContentTypeShouldBeEmpty()\r\n    {\r\n        _contentType.ShouldBeNullOrEmpty();\r\n        _contentTypeHeaderExists.ShouldBe(false);\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\r\n        {\r\n            _contentType = context.Request.ContentType;\r\n            _contentLength = context.Request.ContentLength;\r\n            _contentTypeHeaderExists = context.Request.Headers.TryGetValue(\"Content-Type\", out var value);\r\n            context.Response.StatusCode = (int)statusCode;\r\n            return context.Response.WriteAsync(responseBody);\r\n        });\r\n    }\r\n\r\n    private void GivenThereIsAServiceWithPayloadRunningOn(int port, string basePath, string dummyDatFilePath)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\r\n        {\r\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\r\n            await using var fileStream = File.OpenRead(dummyDatFilePath);\r\n            await fileStream.CopyToAsync(context.Response.Body);\r\n            _memoryUsageAfterCallToService = Process.GetCurrentProcess().WorkingSet64;\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Generates a dummy payload of the given size in MB.\r\n    /// Avoiding maintaining a large file in the repository.\r\n    /// </summary>\r\n    /// <param name=\"sizeInMb\">The file size in MB.</param>\r\n    /// <returns>The payload file path.</returns>\r\n    /// <exception cref=\"ArgumentNullException\">Throwing an exception if the payload path is null.</exception>\r\n    private static string GenerateDummyDatFile(int sizeInMb)\r\n    {\r\n        var payloadName = \"dummy.dat\";\r\n        var payloadPath = Path.Combine(Directory.GetCurrentDirectory(), payloadName);\r\n\r\n        if (File.Exists(payloadPath))\r\n        {\r\n            File.Delete(payloadPath);\r\n        }\r\n\r\n        var newFile = new FileStream(payloadPath, FileMode.CreateNew);\r\n        try\r\n        {\r\n            newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin);\r\n            newFile.WriteByte(0);\r\n        }\r\n        finally\r\n        {\r\n            newFile.Dispose();\r\n        }\r\n\r\n        return payloadPath;\r\n    }\r\n\r\n    private static FileConfiguration GivenConfiguration(int port, string method = null) => new()\r\n    {\r\n        Routes = new()\r\n        {\r\n            new FileRoute\r\n            {\r\n                DownstreamPathTemplate = \"/\",\r\n                DownstreamScheme = Uri.UriSchemeHttp,\r\n                DownstreamHostAndPorts = new()\r\n                {\r\n                    new FileHostAndPort(\"localhost\", port),\r\n                },\r\n                UpstreamPathTemplate = \"/\",\r\n                UpstreamHttpMethod = [method ?? HttpMethods.Get],\r\n            },\r\n        },\r\n    };\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Core/LoadTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Balancers;\nusing System.Diagnostics;\n\nnamespace Ocelot.AcceptanceTests.Core;\n\n/// <summary>\n/// TODO Move to separate Performance Testing (load testing) project.\n/// </summary>\n[Collection(nameof(SequentialTests))]\npublic sealed class LoadTests : ConcurrentSteps\n{\n    private string _downstreamPath;\n    private string _downstreamQuery;\n\n    /// <summary>\n    /// This test should be moved to a separate project. It should be run during release only as an extra check for quality gates.\n    /// </summary>\n    /// <returns>A <see cref=\"Task\"/> representing the asynchronous operation.</returns>\n    [Fact]\n    [Trait(\"PR\", \"1348\")] // https://github.com/ThreeMammals/Ocelot/pull/1348\n    [Trait(\"Bug\", \"2246\")] // https://github.com/ThreeMammals/Ocelot/issues/2246\n    public async Task ShouldLoadRegexCachingHeavily_NoMemoryLeaks()\n    {\n        Assert.SkipWhen(IsCiCd(), \"Skipped in CI/CD! It should be moved to a separate project. It should be run during release only as an extra check for quality gates.\");\n\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, \"/my-gateway/order/{orderNumber}\", \"/order/{orderNumber}\");\n        route.LoadBalancerOptions = new(nameof(NoLoadBalancer));\n\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        GivenThereIsAServiceRunningOn(port, \"/order/\", \"Hello from Donny\");\n\n        // Step 1: Measure memory consumption for constant upstream URL\n        GC.Collect();\n        var a = GC.GetGCMemoryInfo();\n        var totalMemory = ToMegabytes(GC.GetTotalMemory(true));\n        var currentProcess = Process.GetCurrentProcess();\n        var memoryUsage = ToMegabytes(currentProcess.WorkingSet64);\n\n        // Perform 50% of job for stable indicators\n        await WhenIDoActionMultipleTimes(5_000, (i) => WhenIGetUrlOnTheApiGateway(\"/my-gateway/order/1\")); // const url\n        GC.Collect();\n        var totalMemory0 = ToMegabytes(GC.GetTotalMemory(true));\n        var process0 = Process.GetCurrentProcess();\n        var memoryUsage0 = ToMegabytes(process0.WorkingSet64);\n\n        await WhenIDoActionMultipleTimes(5_000, (i) => WhenIGetUrlOnTheApiGateway(\"/my-gateway/order/1\")); // const url\n\n        await WhenIGetUrlOnTheApiGateway(\"/my-gateway/order/1\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n        ThenTheResponseBodyShouldBe(\"Hello from Donny\");\n\n        GC.Collect();\n        var totalMemory1 = ToMegabytes(GC.GetTotalMemory(true));\n        var process1 = Process.GetCurrentProcess();\n        var memoryUsage1 = ToMegabytes(process1.WorkingSet64);\n\n        // Step 2: Measure memory consumption for varying upstream URL\n        // await WhenIDoActionForTime(TimeSpan.FromSeconds(30), (i) => WhenIGetUrlOnTheApiGatewayWithRequestId(\"/my-gateway/order/\" + i)); // varying url\n        await WhenIDoActionMultipleTimes(10_000, (i) => WhenIGetUrlOnTheApiGateway(\"/my-gateway/order/\" + i)); // varying url\n\n        GC.Collect();\n        var totalMemory2 = ToMegabytes(GC.GetTotalMemory(true));\n        var process2 = Process.GetCurrentProcess();\n        var memoryUsage2 = ToMegabytes(process2.WorkingSet64);\n\n        // Assert\n        AssertDelta(totalMemory0, totalMemory1, totalMemory2);\n        AssertDelta(memoryUsage0, memoryUsage1, memoryUsage2);\n    }\n\n    private static float ToMegabytes(long total) => (float)total / (1024 * 1024);\n\n    private static void AssertDelta(float value0, float value1, float value2)\n    {\n        if (value1 <= value0)\n            return;\n\n        var delta = value1 - value0;\n        if (value2 <= value1)\n            return;\n\n        var delta2 = value2 - value1;\n        Assert.True(delta2 <= delta); // delta is not growing\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, string responseBody)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapGet);\n\n        Task MapGet(HttpContext context)\n        {\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value)\n                ? context.Request.PathBase.Value + context.Request.Path.Value\n                : context.Request.Path.Value;\n            _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;\n            var isOK = _downstreamPath.StartsWith(basePath);\n            context.Response.StatusCode = isOK ? (int)HttpStatusCode.OK : (int)HttpStatusCode.NotFound;\n            return context.Response.WriteAsync(isOK ? responseBody : nameof(HttpStatusCode.NotFound));\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Core/ThreadSafeHeadersTests.cs",
    "content": "using System.Collections.Concurrent;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.Core;\n\n// Old integration tests\npublic sealed class ThreadSafeHeadersTests : Steps\n{\n    private readonly ConcurrentBag<ThreadSafeHeadersTestResult> _results;\n\n    public ThreadSafeHeadersTests()\n    {\n        _results = new();\n    }\n\n    [Fact]\n    public void Should_return_same_response_for_each_different_header_under_load_to_downsteam_service()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK);\n        GivenOcelotIsRunning();\n        WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(\"/\", 300);\n        ThenTheSameHeaderValuesAreReturnedByTheDownstreamService();\n    }\n\n    public override void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, [CallerMemberName] string headerKey = nameof(ThreadSafeHeadersTests))\n    {\n        MapStatus_ResponseBody = ctx => ctx.Request.Headers[headerKey].ToString();\n        base.GivenThereIsAServiceRunningOn(port, statusCode, headerKey);\n    }\n\n    private void WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times, [CallerMemberName] string headerKey = nameof(ThreadSafeHeadersTests))\n    {\n        var tasks = new Task[times];\n        for (var i = 0; i < times; i++)\n        {\n            var urlCopy = url;\n            var rint = random.Next(0, 50);\n            tasks[i] = GetForThreadSafeHeadersTest(urlCopy, rint, headerKey);\n        }\n\n        Task.WaitAll(tasks);\n    }\n\n    private async Task GetForThreadSafeHeadersTest(string url, int random, string headerKey)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Get, url);\n        request.Headers.Add(headerKey, [ random.ToString() ]);\n        var response = await ocelotClient.SendAsync(request);\n        var content = await response.Content.ReadAsStringAsync();\n        var result = int.Parse(content);\n        var tshtr = new ThreadSafeHeadersTestResult(result, random);\n        _results.Add(tshtr);\n    }\n\n    private void ThenTheSameHeaderValuesAreReturnedByTheDownstreamService()\n    {\n        foreach (var result in _results)\n        {\n            result.Result.ShouldBe(result.Random);\n        }\n    }\n\n    private class ThreadSafeHeadersTestResult\n    {\n        public ThreadSafeHeadersTestResult(int result, int random)\n        {\n            Result = result;\n            Random = random;\n        }\n\n        public int Result { get; }\n        public int Random { get; }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\r\nusing Microsoft.AspNetCore.Hosting;\r\nusing Microsoft.AspNetCore.Http;\r\nusing Microsoft.AspNetCore.TestHost;\r\nusing Ocelot.Middleware;\r\nusing System.Diagnostics;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic class CustomMiddlewareTests : Steps\r\n{\r\n    private int _counter;\r\n\r\n    public CustomMiddlewareTests()\r\n    {\r\n        _counter = 0;\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_pre_query_string_builder_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            AuthorizationMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_authorization_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            AuthorizationMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_authentication_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            AuthenticationMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/\", \"/41879/\");\r\n        var configuration = GivenConfiguration(route);\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_pre_error_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            PreErrorResponderMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_pre_authorization_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            PreAuthorizationMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_pre_http_authentication_middleware()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            PreAuthenticationMiddleware = async (ctx, next) =>\r\n            {\r\n                _counter++;\r\n                await next.Invoke();\r\n            },\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_throw_when_pipeline_terminates_early()\r\n    {\r\n        var pipelineConfiguration = new OcelotPipelineConfiguration\r\n        {\r\n            PreQueryStringBuilderMiddleware = (context, next) =>\r\n                Task.Run(() =>\r\n                {\r\n                    _counter++;\r\n                    return; // do not invoke the rest of the pipeline\r\n                }),\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => x.ThenTheCounterIs(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    /// <summary>\r\n    /// This is just an example to show how you could hook into Ocelot pipeline with your own middleware.\r\n    /// At the moment you must use Response.OnCompleted callback and cannot change the response :(\r\n    /// I will see if this can be changed one day.\r\n    /// </summary>\r\n    [Fact]\r\n    [Trait(\"Feat\", \"237\")] // https://github.com/ThreeMammals/Ocelot/issues/237\r\n    [Trait(\"PR\", \"241\")] // https://github.com/ThreeMammals/Ocelot/pull/241\r\n    [Trait(\"Release\", \"3.1.6\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.6\r\n    public void Should_fix_issue_237()\r\n    {\r\n        Func<object, Task> callback = state =>\r\n        {\r\n            var context = (HttpContext)state;\r\n            if (context.Response.StatusCode > 400)\r\n            {\r\n                Debug.WriteLine(\"COUNT CALLED\");\r\n                Console.WriteLine(\"COUNT CALLED\");\r\n            }\r\n            return Task.CompletedTask;\r\n        };\r\n\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/\", \"/west\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, \"/test\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithMiddlewareBeforePipeline<FakeMiddleware>(callback))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    private Task<int> GivenOcelotIsRunningWithMiddlewareBeforePipeline<T>(Func<object, Task> middleware)\r\n        => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot,\r\n            async app => await app.UseMiddleware<T>(middleware).UseOcelot());\r\n\r\n    private void ThenTheCounterIs(int expected)\r\n    {\r\n        _counter.ShouldBe(expected);\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOnPath(int port, string basePath)\r\n    {\r\n        Task MapPath(HttpContext context)\r\n        {\r\n            if (string.IsNullOrEmpty(basePath))\r\n            {\r\n                context.Response.StatusCode = (int)HttpStatusCode.OK;\r\n            }\r\n            else if (context.Request.Path.Value != basePath)\r\n            {\r\n                context.Response.StatusCode = (int)HttpStatusCode.NotFound;\r\n            }\r\n            return Task.CompletedTask;\r\n        }\r\n        handler.GivenThereIsAServiceRunningOn(port, MapPath);\r\n    }\r\n\r\n    public class FakeMiddleware\r\n    {\r\n        private readonly RequestDelegate _next;\r\n        private readonly Func<object, Task> _callback;\r\n\r\n        public FakeMiddleware(RequestDelegate next, Func<object, Task> callback)\r\n        {\r\n            _next = next;\r\n            _callback = callback;\r\n        }\r\n\r\n        public async Task Invoke(HttpContext context)\r\n        {\r\n            await _next(context);\r\n\r\n            context.Response.OnCompleted(_callback, context);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\r\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests;\n\n[Trait(\"Feat\", \"1672\")]\r\npublic sealed class DefaultVersionPolicyTests : Steps\n{\n    public DefaultVersionPolicyTests()\n    {\n    }\n\n    [Fact]\n    public void Should_return_bad_gateway_when_request_higher_receive_lower()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"2.0\", VersionPolicies.RequestVersionOrHigher);\n        var configuration = GivenConfiguration(route);\r\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_bad_gateway_when_request_lower_receive_higher()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"1.1\", VersionPolicies.RequestVersionOrLower);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_bad_gateway_when_request_exact_receive_different()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"1.1\", VersionPolicies.RequestVersionExact);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\r\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_request_version_exact_receive_exact()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"2.0\", VersionPolicies.RequestVersionExact);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_request_version_lower_receive_lower()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"2.0\", VersionPolicies.RequestVersionOrLower);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_request_version_lower_receive_exact()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"2.0\", VersionPolicies.RequestVersionOrLower);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\r\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_request_version_higher_receive_higher()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"1.1\", VersionPolicies.RequestVersionOrHigher);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_request_version_higher_receive_exact()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenHttpsRoute(port, \"1.1\", VersionPolicies.RequestVersionOrHigher);\n        var configuration = GivenConfiguration(route);\n        var body = Body();\n        this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\r\n\n    private void GivenThereIsHttpsServiceRunningOn(int port, HttpProtocols protocols, [CallerMemberName] string body = \"supercalifragilistic\")\n    {\r\n        var url = DownstreamUrl(port, Uri.UriSchemeHttps);\r\n        handler.GivenThereIsAServiceRunningOnWithKestrelOptions(url, string.Empty,\r\n            options => options.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = protocols; }),\r\n            context =>\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.OK;\n                return context.Response.WriteAsync(body);\n            });\n    }\n\n    private static FileRoute GivenHttpsRoute(int port, string httpVersion, string versionPolicy) => new()\n    {\n        UpstreamPathTemplate = \"/\",\n        UpstreamHttpMethod = [HttpMethods.Get],\n        DownstreamPathTemplate = \"/\",\n        DownstreamHostAndPorts = new() { new(\"localhost\", port) },\n        DownstreamScheme = Uri.UriSchemeHttps, // !!!\n        DownstreamHttpVersion = httpVersion,\n        DownstreamHttpVersionPolicy = versionPolicy,\n        DangerousAcceptAnyServerCertificateValidator = true,\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/GzipTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\r\nusing Newtonsoft.Json;\r\nusing System.IO.Compression;\r\nusing System.Net.Http.Headers;\r\nusing System.Text;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class GzipTests : Steps\r\n{\r\n    public GzipTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Post);\r\n        var configuration = GivenConfiguration(route);\r\n        var input = \"people\";\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\", \"\\\"people\\\"\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", GivenThePostHasGzipContent(input)))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    private static StreamContent GivenThePostHasGzipContent(object input)\r\n    {\r\n        var json = JsonConvert.SerializeObject(input);\r\n        var jsonBytes = Encoding.UTF8.GetBytes(json);\r\n        var ms = new MemoryStream();\r\n        using (var gzip = new GZipStream(ms, CompressionMode.Compress, true))\r\n        {\r\n            gzip.Write(jsonBytes, 0, jsonBytes.Length);\r\n        }\r\n\r\n        ms.Position = 0;\r\n        var content = new StreamContent(ms);\r\n        content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\r\n        content.Headers.ContentEncoding.Add(\"gzip\");\r\n        return content;\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody, string expected)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\r\n        {\r\n            if (context.Request.Headers.TryGetValue(\"Content-Encoding\", out var contentEncoding))\r\n            {\r\n                contentEncoding.First().ShouldBe(\"gzip\");\r\n\r\n                string text = null;\r\n                using (var decompress = new GZipStream(context.Request.Body, CompressionMode.Decompress))\r\n                {\r\n                    using var sr = new StreamReader(decompress);\r\n                    text = await sr.ReadToEndAsync();\r\n                }\r\n                if (text != expected)\r\n                {\r\n                    throw new Exception(\"not gzipped\");\r\n                }\r\n\r\n                context.Response.StatusCode = (int)statusCode;\r\n                await context.Response.WriteAsync(responseBody);\r\n            }\r\n            else\r\n            {\r\n                context.Response.StatusCode = (int)HttpStatusCode.NotFound;\r\n                await context.Response.WriteAsync(\"downstream path didnt match base path\");\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.DependencyInjection;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class HttpDelegatingHandlersTests : Steps\r\n{\r\n    private string _downstreamPath;\r\n\r\n    public HttpDelegatingHandlersTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_route_ordered_specific_handlers()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        route.DelegatingHandlers = new()\r\n        {\r\n            \"FakeHandlerTwo\",\r\n            \"FakeHandler\",\r\n        };\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi<FakeHandler, FakeHandlerTwo>())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .And(x => ThenTheOrderedHandlersAreCalledCorrectly())\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_global_di_handlers()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi<FakeHandler, FakeHandlerTwo>())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .And(x => ThenTheHandlersAreCalledCorrectly())\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_global_di_handlers_multiple_times()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithDelegatingHandler<FakeHandlerAgain>(true))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_call_global_di_handlers_with_dependency()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        var dependency = new FakeDependency();\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi<FakeHandlerWithDependency>(dependency))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .And(x => ThenTheDependencyIsCalled(dependency))\r\n            .BDDfy();\r\n    }\r\n\r\n    private static FileRoute GivenRoute(int port) => new()\r\n    {\r\n        DownstreamPathTemplate = \"/\",\r\n        DownstreamScheme = Uri.UriSchemeHttp,\r\n        DownstreamHostAndPorts = new()\r\n        {\r\n            new(\"localhost\", port),\r\n        },\r\n        UpstreamPathTemplate = \"/\",\r\n        UpstreamHttpMethod = [HttpMethods.Get],\r\n    };\r\n\r\n    private void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi<THandler1, THandler2>()\r\n        where THandler1 : DelegatingHandler\r\n        where THandler2 : DelegatingHandler\r\n    {\r\n        GivenOcelotIsRunning(s => s\r\n            .AddOcelot()\r\n            .AddDelegatingHandler<THandler1>()\r\n            .AddDelegatingHandler<THandler2>());\r\n    }\r\n\r\n    private void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi<THandler1, THandler2>()\r\n        where THandler1 : DelegatingHandler\r\n        where THandler2 : DelegatingHandler\r\n    {\r\n        GivenOcelotIsRunning(s => s\r\n            .AddOcelot()\r\n            .AddDelegatingHandler<THandler1>(true)\r\n            .AddDelegatingHandler<THandler2>(true));\r\n    }\r\n\r\n    private void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi<THandler>(FakeDependency dependency)\r\n        where THandler : DelegatingHandler\r\n    {\r\n        GivenOcelotIsRunning(s => s\r\n            .AddSingleton(dependency)\r\n            .AddOcelot()\r\n            .AddDelegatingHandler<THandler>(true));\r\n    }\r\n\r\n    private static void ThenTheDependencyIsCalled(FakeDependency dependency)\r\n        => dependency.Called.ShouldBeTrue();\r\n    private static void ThenTheHandlersAreCalledCorrectly()\r\n        => FakeHandler.TimeCalled.ShouldBeLessThan(FakeHandlerTwo.TimeCalled);\r\n    private static void ThenTheOrderedHandlersAreCalledCorrectly()\r\n        => FakeHandlerTwo.TimeCalled.ShouldBeLessThan(FakeHandler.TimeCalled);\r\n\r\n    public class FakeDependency\r\n    {\r\n        public bool Called;\r\n    }\r\n\r\n    // ReSharper disable once ClassNeverInstantiated.Local\r\n    private class FakeHandlerWithDependency : DelegatingHandler\r\n    {\r\n        private readonly FakeDependency _dependency;\r\n\r\n        public FakeHandlerWithDependency(FakeDependency dependency)\r\n        {\r\n            _dependency = dependency;\r\n        }\r\n\r\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n        {\r\n            _dependency.Called = true;\r\n            return base.SendAsync(request, cancellationToken);\r\n        }\r\n    }\r\n\r\n    // ReSharper disable once ClassNeverInstantiated.Local\r\n    private class FakeHandler : DelegatingHandler\r\n    {\r\n        public static DateTime TimeCalled { get; private set; }\r\n\r\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellation)\r\n        {\r\n            TimeCalled = DateTime.Now;\r\n            await Task.Delay(TimeSpan.FromMilliseconds(10), cancellation);\r\n            return await base.SendAsync(request, cancellation);\r\n        }\r\n    }\r\n\r\n    // ReSharper disable once ClassNeverInstantiated.Local\r\n    private class FakeHandlerTwo : DelegatingHandler\r\n    {\r\n        public static DateTime TimeCalled { get; private set; }\r\n\r\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellation)\r\n        {\r\n            TimeCalled = DateTime.Now;\r\n            await Task.Delay(TimeSpan.FromMilliseconds(10), cancellation);\r\n            return await base.SendAsync(request, cancellation);\r\n        }\r\n    }\r\n\r\n    // ReSharper disable once ClassNeverInstantiated.Local\r\n    private class FakeHandlerAgain : DelegatingHandler\r\n    {\r\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellation)\r\n        {\r\n            Console.WriteLine(request.RequestUri);\r\n\r\n            //do stuff and optionally call the base handler..\r\n            return await base.SendAsync(request, cancellation);\r\n        }\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\r\n        {\r\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\r\n            bool match = _downstreamPath == basePath;\r\n            context.Response.StatusCode = match ? (int)statusCode : (int)HttpStatusCode.NotFound;\r\n            return context.Response.WriteAsync(match ? responseBody : nameof(HttpStatusCode.NotFound));\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/CookieStickySessionsTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.LoadBalancer.Balancers;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\n[Trait(\"Feat\", \"336\")] // https://github.com/ThreeMammals/Ocelot/pull/336\npublic sealed class CookieStickySessionsTests : Steps\n{\n    private int[] _counters;\n#if NET9_0_OR_GREATER\n    private static readonly Lock SyncLock = new();\n#else\n    private static readonly object SyncLock = new();\n#endif\n\n    public CookieStickySessionsTests() : base()\n    {\n        _counters = new int[2];\n    }\n\n    [Fact]\n    public void ShouldUseSameDownstreamHost_ForSingleRouteWithHighLoad()\n    {\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route = GivenStickySessionsRoute([port1, port2]);\n        var cookieName = route.LoadBalancerOptions.Key;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenProductServiceIsRunning(0, port1))\n            .Given(x => x.GivenProductServiceIsRunning(1, port2))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning())\n            .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/\", 10, cookieName, Guid.NewGuid().ToString()))\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 10)) // RoundRobin should return first service with port1\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void ShouldUseDifferentDownstreamHost_ForDoubleRoutesWithDifferentCookies()\n    {\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route1 = GivenStickySessionsRoute([port1, port2]);\n        var cookieName = route1.LoadBalancerOptions.Key;\n        var route2 = GivenStickySessionsRoute([port2, port1], \"/test\", cookieName + \"bestid\");\n        var configuration = GivenConfiguration(route1, route2);\n\n        this.Given(x => x.GivenProductServiceIsRunning(0, port1))\n            .Given(x => x.GivenProductServiceIsRunning(1, port2))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning())\n            .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie(\"/\", cookieName, \"123\")) // both cookies should have different values\n            .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie(\"/test\", cookieName + \"bestid\", \"123\")) // stick by cookie value\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 1))\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 1))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void ShouldUseSameDownstreamHost_ForDifferentRoutesWithSameCookie()\n    {\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route1 = GivenStickySessionsRoute([port1, port2]);\n        var cookieName = route1.LoadBalancerOptions.Key;\n        var route2 = GivenStickySessionsRoute([port2, port1], \"/test\", cookieName);\n        var configuration = GivenConfiguration(route1, route2);\n\n        this.Given(x => x.GivenProductServiceIsRunning(0, port1))\n            .Given(x => x.GivenProductServiceIsRunning(1, port2))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning())\n            .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie(\"/\", cookieName, \"123\"))\n            .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie(\"/test\", cookieName, \"123\"))\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 2))\n            .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public async Task ShouldUseGlobalOptions_ForStaticRoutes()\n    {\n        _counters = new int[5];\n        var ports = PortFinder.GetPorts(2);\n        var route1 = GivenStickySessionsRoute(ports);\n        route1.LoadBalancerOptions = new(); // no load balancing -> use global opts\n        var route2 = GivenStickySessionsRoute(ports.Reverse().ToArray(), \"/test\");\n        route1.LoadBalancerOptions = new(); // no load balancing -> use global opts\n        var ports2 = PortFinder.GetPorts(2);\n        var route3 = GivenStickySessionsRoute(ports2, \"/nextSticky\", CookieName() + \"-nextSticky\");\n        var port5 = PortFinder.GetRandomPort();\n        var route4 = GivenStickySessionsRoute([port5], \"/noLoadBalancing\"); // this route should not be overwritten by global LB opts\n        route4.LoadBalancerOptions.Type = nameof(NoLoadBalancer);\n\n        var configuration = GivenConfiguration(route1, route2, route3, route4); // static routes come to Routes collection\n        configuration.GlobalConfiguration.LoadBalancerOptions = new()\n        {\n            Type = nameof(CookieStickySessions),\n            Key = CookieName(), // !!!\n        };\n        GivenProductServiceIsRunning(0, ports[0]);\n        GivenProductServiceIsRunning(1, ports[1]);\n        GivenProductServiceIsRunning(2, ports2[0]);\n        GivenProductServiceIsRunning(3, ports2[1]);\n        GivenProductServiceIsRunning(4, port5);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayWithCookie(\"/\", CookieName(), \"123\");\n        await WhenIGetUrlOnTheApiGatewayWithCookie(\"/test\", CookieName(), \"123\");\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/nextSticky\", 5, CookieName() + \"-nextSticky\", \"333\");\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/noLoadBalancing\", 7, \"bla-bla-cookie\", \"bla-bla-value\");\n        ThenServiceShouldHaveBeenCalledTimes(0, 2);\n        ThenServiceShouldHaveBeenCalledTimes(1, 0);\n        ThenServiceShouldHaveBeenCalledTimes(2, 5);\n        ThenServiceShouldHaveBeenCalledTimes(3, 0);\n        ThenServiceShouldHaveBeenCalledTimes(4, 7);\n    }\n\n    private static string CookieName([CallerMemberName] string cookieName = nameof(CookieStickySessionsTests)) => cookieName;\n\n    private FileRoute GivenStickySessionsRoute(int[] ports, string upstream = null, [CallerMemberName] string cookieName = null)\n    {\n        var route = GivenRoute(ports[0], upstream: upstream ?? \"/\");\n        route.DownstreamHostAndPorts = ports.Select(Localhost).ToList();\n        route.LoadBalancerOptions = new()\n        {\n            Type = nameof(CookieStickySessions),\n            Key = cookieName, // !!!\n            Expiry = 300_000,\n        };\n        return route;\n    }\n\n    private Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value)\n    {\n        var tasks = new Task[times];\n        for (var i = 0; i < times; i++)\n        {\n            tasks[i] = GetParallelTask(url, cookie, value);\n        }\n\n        return Task.WhenAll(tasks);\n    }\n\n    private async Task GetParallelTask(string url, string cookie, string value)\n    {\n        var response = await WhenIGetUrlOnTheApiGateway(url, cookie, value);\n        var content = await response.Content.ReadAsStringAsync();\n        var count = int.Parse(content);\n        count.ShouldBeGreaterThan(0);\n    }\n\n    private void ThenServiceShouldHaveBeenCalledTimes(int index, int times)\n    {\n        _counters[index].ShouldBe(times);\n    }\n\n    private void GivenProductServiceIsRunning(int index, int port)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, async context =>\n        {\n            try\n            {\n                string response;\n                lock (SyncLock)\n                {\n                    _counters[index]++;\n                    response = _counters[index].ToString();\n                }\n\n                context.Response.StatusCode = (int)HttpStatusCode.OK;\n                await context.Response.WriteAsync(response);\n            }\n            catch (Exception exception)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;\n                await context.Response.WriteAsync(exception.StackTrace);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/ILoadBalancerAnalyzer.cs",
    "content": "﻿using Ocelot.LoadBalancer;\nusing Ocelot.Values;\nusing System.Collections.Concurrent;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\npublic interface ILoadBalancerAnalyzer\n{\n    string ServiceName { get; }\n    string GenerationPrefix { get; }\n    ConcurrentBag<LeaseEventArgs> Events { get; }\n    object Analyze();\n    Dictionary<ServiceHostAndPort, int> GetHostCounters();\n    Dictionary<ServiceHostAndPort, int> ToHostCountersDictionary(IEnumerable<IGrouping<ServiceHostAndPort, LeaseEventArgs>> grouping);\n    bool HasManyServiceGenerations(int maxGeneration);\n    int BottomOfConnections();\n    int TopOfConnections();\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\ninternal sealed class LeastConnectionAnalyzer : LoadBalancerAnalyzer, ILoadBalancer\n{\n    private readonly LeastConnection loadBalancer;\n\n    public LeastConnectionAnalyzer(Func<Task<List<Service>>> services, string serviceName)\n        : base(serviceName)\n    {\n        loadBalancer = new(services, serviceName);\n        loadBalancer.Leased += Me_Leased;\n    }\n\n    private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args);\n\n    public override string Type => nameof(LeastConnectionAnalyzer);\n    public override Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => loadBalancer.LeaseAsync(httpContext);\n    public override void Release(ServiceHostAndPort hostAndPort) => loadBalancer.Release(hostAndPort);\n\n    public override Dictionary<ServiceHostAndPort, int> ToHostCountersDictionary(IEnumerable<IGrouping<ServiceHostAndPort, LeaseEventArgs>> grouping)\n        => grouping.ToDictionary(g => g.Key, g => g.Count(e => e.Lease == g.Key));\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\ninternal sealed class LeastConnectionAnalyzerCreator : ILoadBalancerCreator\n{\n    // We need to adhere to the same implementations of RoundRobinCreator, which results in a significant design overhead, (until redesigned)\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        var loadBalancer = new LeastConnectionAnalyzer(\n            serviceProvider.GetAsync,\n            !string.IsNullOrEmpty(route.ServiceName) // if service discovery mode then use service name; otherwise use balancer key\n                ? route.ServiceName\n                : route.LoadBalancerKey);\n        return new OkResponse<ILoadBalancer>(loadBalancer);\n    }\n\n    public string Type => nameof(LeastConnectionAnalyzer);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Collections.Concurrent;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\ninternal class LoadBalancerAnalyzer : ILoadBalancerAnalyzer, ILoadBalancer\n{\n    protected readonly string _serviceName;\n    protected LoadBalancerAnalyzer(string serviceName) => _serviceName = serviceName;\n\n    public string ServiceName => _serviceName;\n    public virtual string GenerationPrefix => \"Gen:\";\n    public ConcurrentBag<LeaseEventArgs> Events { get; } = new();\n\n    public virtual object Analyze()\n    {\n        var allGenerations = Events\n            .Select(e => e.Service.Tags.FirstOrDefault(t => t.StartsWith(GenerationPrefix)))\n            .Where(generation => !string.IsNullOrEmpty(generation))\n            .Distinct().ToArray();\n        var allIndices = Events.Select(e => e.ServiceIndex)\n            .Distinct().OrderBy(index => index).ToArray();\n\n        Dictionary<string, List<LeaseEventArgs>> eventsPerGeneration = new();\n        foreach (var generation in allGenerations)\n        {\n            var l = Events.Where(e => e.Service.Tags.Contains(generation)).ToList();\n            eventsPerGeneration.Add(generation, l);\n        }\n\n        Dictionary<string, List<int>> generationIndices = new();\n        foreach (var generation in allGenerations)\n        {\n            var l = eventsPerGeneration[generation].Select(e => e.ServiceIndex).Distinct().ToList();\n            generationIndices.Add(generation, l);\n        }\n\n        Dictionary<string, List<Lease>> generationLeases = new();\n        foreach (var generation in allGenerations)\n        {\n            var l = eventsPerGeneration[generation].Select(e => e.Lease).ToList();\n            generationLeases.Add(generation, l);\n        }\n\n        Dictionary<string, List<ServiceHostAndPort>> generationHosts = new();\n        foreach (var generation in allGenerations)\n        {\n            var l = eventsPerGeneration[generation].Select(e => e.Lease.HostAndPort).Distinct().ToList();\n            generationHosts.Add(generation, l);\n        }\n\n        Dictionary<string, List<Lease>> generationLeasesWithMaxConnections = new();\n        foreach (var generation in allGenerations)\n        {\n            List<Lease> leases = new();\n            var uniqueHosts = generationHosts[generation];\n            foreach (var host in uniqueHosts)\n            {\n                int max = generationLeases[generation].Where(l => l == host).Max(l => l.Connections);\n                Lease wanted = generationLeases[generation].Find(l => l == host && l.Connections == max);\n                leases.Add(wanted);\n            }\n\n            leases = leases.OrderBy(l => l.HostAndPort.DownstreamPort).ToList();\n            generationLeasesWithMaxConnections.Add(generation, leases);\n        }\n\n        return generationLeasesWithMaxConnections;\n    }\n\n    public virtual bool HasManyServiceGenerations(int maxGeneration)\n    {\n        int[] generations = new int[maxGeneration + 1];\n        string[] tags = new string[maxGeneration + 1];\n        for (int i = 0; i < generations.Length; i++)\n        {\n            generations[i] = i;\n            tags[i] = GenerationPrefix + i;\n        }\n\n        var all = Events\n            .Select(e => e.Service.Tags.FirstOrDefault(t => t.StartsWith(GenerationPrefix)))\n            .Distinct().ToArray();\n        return all.All(tags.Contains);\n    }\n\n    public virtual Dictionary<ServiceHostAndPort, int> GetHostCounters()\n    {\n        var hosts = Events.Select(e => e.Lease.HostAndPort).Distinct().ToList();\n        var grouping = Events\n            .GroupBy(e => e.Lease.HostAndPort)\n            .OrderBy(g => g.Key.DownstreamPort);\n        return ToHostCountersDictionary(grouping);\n    }\n\n    public virtual Dictionary<ServiceHostAndPort, int> ToHostCountersDictionary(IEnumerable<IGrouping<ServiceHostAndPort, LeaseEventArgs>> grouping)\n        => grouping.ToDictionary(g => g.Key, g => g.Count(e => e.Lease == g.Key));\n\n    public virtual int BottomOfConnections()\n    {\n        var hostCounters = GetHostCounters();\n        return hostCounters.Min(_ => _.Value);\n    }\n\n    public virtual int TopOfConnections()\n    {\n        var hostCounters = GetHostCounters();\n        return hostCounters.Max(_ => _.Value);\n    }\n\n    public virtual string Type => nameof(LoadBalancerAnalyzer);\n    public virtual Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n        => Task.FromResult<Response<ServiceHostAndPort>>(new ErrorResponse<ServiceHostAndPort>(new UnableToFindLoadBalancerError(GetType().Name)));\n    public virtual void Release(ServiceHostAndPort hostAndPort) { }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\npublic sealed class LoadBalancerTests : ConcurrentSteps\n{\n    [Theory]\n    [Trait(\"Feat\", \"211\")]\n    [InlineData(false)] // original scenario, clean config\n    [InlineData(true)] // extended scenario using analyzer\n    public void ShouldLoadBalanceRequestWithLeastConnection(bool withAnalyzer)\n    {\n        var ports = PortFinder.GetPorts(2);\n        var route = GivenLbRoute(ports, withAnalyzer ? nameof(LeastConnectionAnalyzer) : nameof(LeastConnection));\n        var configuration = GivenConfiguration(route);\n        var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray();\n        LeastConnectionAnalyzer lbAnalyzer = null;\n        LeastConnectionAnalyzer getAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider)\n        {\n            //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse\n            return lbAnalyzer ??= new LeastConnectionAnalyzerCreator().Create(route, provider)?.Data as LeastConnectionAnalyzer;\n        }\n        Action<IServiceCollection> withLeastConnectionAnalyzer = (s)\n            => s.AddOcelot().AddCustomLoadBalancer<LeastConnectionAnalyzer>(getAnalyzer);\n        GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(withAnalyzer ? withLeastConnectionAnalyzer : WithAddOcelot))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 99))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(99))\n            .And(x => ThenAllServicesCalledOptimisticAmountOfTimes(lbAnalyzer))\n            .And(x => ThenServiceCountersShouldMatchLeasingCounters(lbAnalyzer, ports, 99))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(\n#if NET10_0_OR_GREATER\n                Bottom(99, ports.Length) - 3, Top(99, ports.Length) + 3\n#else\n                Bottom(99, ports.Length), Top(99, ports.Length)\n#endif\n                ))\n            // .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 49)) // strict assertion, this is ideal case when load is not high\n            .And(x => _counters.ShouldAllBe(c =>\n#if NET10_0_OR_GREATER\n                c <= 53 && c >= 46,\n#else\n                c == 50 || c == 49,\n#endif\n                CalledTimesMessage())) // LeastConnection algorithm distributes counters as 49/50 or 50/49 depending on thread synchronization\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"365\")]\n    [InlineData(false)] // original scenario, clean config\n    [InlineData(true)] // extended scenario using analyzer\n    public void ShouldLoadBalanceRequestWithRoundRobin(bool withAnalyzer)\n    {\n        var ports = PortFinder.GetPorts(2);\n        var route = GivenLbRoute(ports, withAnalyzer ? nameof(RoundRobinAnalyzer) : nameof(RoundRobin));\n        var configuration = GivenConfiguration(route);\n        var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray();\n        RoundRobinAnalyzer lbAnalyzer = null;\n        RoundRobinAnalyzer getAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider)\n        {\n            //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse\n            return lbAnalyzer ??= new RoundRobinAnalyzerCreator().Create(route, provider)?.Data as RoundRobinAnalyzer;\n        }\n        Action<IServiceCollection> withRoundRobinAnalyzer = (s)\n            => s.AddOcelot().AddCustomLoadBalancer<RoundRobinAnalyzer>(getAnalyzer);\n        GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(withAnalyzer ? withRoundRobinAnalyzer : WithAddOcelot))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 99))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(99))\n            .And(x => ThenAllServicesCalledOptimisticAmountOfTimes(lbAnalyzer))\n            .And(x => ThenServiceCountersShouldMatchLeasingCounters(lbAnalyzer, ports, 99))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(99, ports.Length), Top(99, ports.Length)))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 49)) // strict assertion\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"961\")]\n    public void ShouldLoadBalanceRequestWithCustomLoadBalancer()\n    {\n        Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, CustomLoadBalancer> loadBalancerFactoryFunc =\n            (serviceProvider, route, discoveryProvider) => new CustomLoadBalancer(discoveryProvider.GetAsync);\n        var ports = PortFinder.GetPorts(2);\n        var route = GivenLbRoute(ports, nameof(CustomLoadBalancer));\n        var configuration = GivenConfiguration(route);\n        var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray();\n        Action<IServiceCollection> withCustomLoadBalancer = (s)\n            => s.AddOcelot().AddCustomLoadBalancer<CustomLoadBalancer>(loadBalancerFactoryFunc);\n        GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(withCustomLoadBalancer))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 50))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(50, ports.Length), Top(50, ports.Length)))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(25, 25)) // strict assertion\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void ShouldApplyGlobalOptions_ForStaticRoutes()\n    {\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(ports1, upstream: \"/route1\");\n        route1.LoadBalancerOptions = new(); // no load balancing -> use global opts\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(ports2, nameof(LeastConnection), \"/route2\");\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), \"/noLoadBalancing\");\n\n        var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection\n        configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin));\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route1\", 2);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route2\", 5);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/noLoadBalancing\", 7);\n\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 3, 2, 7, 0); // main assertion, explanation is below\n        ThenServiceShouldHaveBeenCalledTimes(0, 1); // RoundRobin for 2\n        ThenServiceShouldHaveBeenCalledTimes(1, 1); // RoundRobin for 2\n        ThenServiceShouldHaveBeenCalledTimes(2, 3); // LeastConnection for 5\n        ThenServiceShouldHaveBeenCalledTimes(3, 2); // LeastConnection for 5\n        ThenServiceShouldHaveBeenCalledTimes(4, 7); // NoLoadBalancer for 7\n        ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 7\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void ShouldApplyGlobalGroupOptions_ForStaticRoutes_WhenRouteOptsHasAKey()\n    {\n        // 1st route\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(ports1, upstream: \"/route1\");\n        route1.LoadBalancerOptions = null; // 1st route is not balanced\n        route1.Key = null; // 1st route is not in the global group\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(ports2, upstream: \"/route2\");\n        route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones\n        route2.Key = \"R2\"; // 2nd route is in the group\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), \"/noLoadBalancing\");\n\n        var configuration = GivenConfiguration(route1, route2, route3);\n        configuration.GlobalConfiguration.LoadBalancerOptions = new()\n        {\n            RouteKeys = [\"R2\"],\n            Type = nameof(RoundRobin),\n        };\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route1\", 2);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route2\", 4);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/noLoadBalancing\", 5);\n        ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below\n        ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2\n        ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2\n        ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4\n        ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4\n        ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5\n        ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5\n    }\n\n    private sealed class CustomLoadBalancer : ILoadBalancer\n    {\n        private readonly Func<Task<List<Service>>> _services;\n#if NET9_0_OR_GREATER\n        private static readonly Lock _lock = new();\n#else\n        private static readonly object _lock = new();\n#endif\n        private int _last;\n\n        public string Type => nameof(CustomLoadBalancer);\n        public CustomLoadBalancer(Func<Task<List<Service>>> services) => _services = services;\n\n        public async Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext)\n        {\n            var services = await _services();\n            lock (_lock)\n            {\n                if (_last >= services.Count) _last = 0;\n                var next = services[_last++];\n                return new OkResponse<ServiceHostAndPort>(next.HostAndPort);\n            }\n        }\n\n        public void Release(ServiceHostAndPort hostAndPort) { }\n    }\n\n    private FileRoute GivenLbRoute(int[] ports, string loadBalancer = null, string upstream = null)\n    {\n        var route = GivenRoute(ports[0], upstream: upstream);\n        route.DownstreamHostAndPorts = ports.Select(Localhost).ToList();\n        route.LoadBalancerOptions = new(loadBalancer ?? nameof(LeastConnection));\n        return route;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs",
    "content": "﻿using KubeClient.Models;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\ninternal sealed class RoundRobinAnalyzer : LoadBalancerAnalyzer, ILoadBalancer\n{\n    private readonly RoundRobin loadBalancer;\n\n    public RoundRobinAnalyzer(Func<Task<List<Service>>> services, string serviceName)\n        : base(serviceName)\n    {\n        loadBalancer = new(services, serviceName);\n        loadBalancer.Leased += Me_Leased;\n    }\n\n    private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args);\n\n    public override string Type => nameof(RoundRobinAnalyzer);\n    public override Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => loadBalancer.LeaseAsync(httpContext);\n    public override void Release(ServiceHostAndPort hostAndPort) => loadBalancer.Release(hostAndPort);\n\n    public override string GenerationPrefix => nameof(EndpointsV1.Metadata.Generation) + \":\";\n\n    public override Dictionary<ServiceHostAndPort, int> ToHostCountersDictionary(IEnumerable<IGrouping<ServiceHostAndPort, LeaseEventArgs>> grouping)\n        => grouping.ToDictionary(g => g.Key, g => g.Max(e => e.Lease.Connections));\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.AcceptanceTests.LoadBalancer;\n\ninternal sealed class RoundRobinAnalyzerCreator : ILoadBalancerCreator\n{\n    // We need to adhere to the same implementations of RoundRobinCreator, which results in a significant design overhead, (until redesigned)\n    public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n    {\n        var loadBalancer = new RoundRobinAnalyzer(\n            serviceProvider.GetAsync,\n            !string.IsNullOrEmpty(route.ServiceName) // if service discovery mode then use service name; otherwise use balancer key\n                ? route.ServiceName\n                : route.LoadBalancerKey);\n        return new OkResponse<ILoadBalancer>(loadBalancer);\n    }\n\n    public string Type => nameof(RoundRobinAnalyzer);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/LogLevelTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.TestHost;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Serilog;\nusing Serilog.Core;\n\nnamespace Ocelot.AcceptanceTests;\n\npublic sealed class LogLevelTests : Steps\n{\n    private readonly string _logFileName;\n    private readonly string _appSettingsFileName;\n\n    private const string AppSettingsFormat =\n        \"{{\\\"Logging\\\":{{\\\"LogLevel\\\":{{\\\"Default\\\":\\\"{0}\\\",\\\"System\\\":\\\"{0}\\\",\\\"Microsoft\\\":\\\"{0}\\\"}}}}}}\";\n\n    public LogLevelTests()\n    {\n        _logFileName = $\"ocelot_logs_{TestID}.log\";\n        _appSettingsFileName = $\"appsettings_{TestID}.json\";\n        Files.Add(_logFileName);\n        Files.Add(_appSettingsFileName);\n    }\n\n    private void ThenMessagesAreLogged(string[] notAllowedMessageTypes, string[] allowedMessageTypes)\n    {\n        var logFilePath = GetLogFilePath();\n        var logFileContent = File.ReadAllText(logFilePath);\n        var logFileLines = logFileContent.Split(Environment.NewLine);\n\n        var logFileLinesWithLogLevel = logFileLines.Where(x => notAllowedMessageTypes.Any(x.Contains)).ToList();\n        logFileLinesWithLogLevel.Count.ShouldBe(0);\n\n        var logFileLinesWithAllowedLogLevel = logFileLines.Where(x => allowedMessageTypes.Any(x.Contains)).ToList();\n        logFileLinesWithAllowedLogLevel.Count.ShouldBe(2 * allowedMessageTypes.Length);\n    }\n\n    private void TestFactory(string[] notAllowedMessageTypes, string[] allowedMessageTypes, LogLevel level)\n    {\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/\",\n                    DownstreamHostAndPorts = new List<FileHostAndPort>\n                    {\n                        new()\n                        {\n                            Host = \"localhost\",\n                            Port = port,\n                        },\n                    },\n                    DownstreamScheme = \"http\",\n                    UpstreamPathTemplate = \"/\",\n                    UpstreamHttpMethod = [\"Get\"],\n                    RequestIdKey = \"Oc-RequestId\",\n                },\n            },\n        };\n\n        using var logger = GetLogger(level);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunningWithMinimumLogLevel(logger, _appSettingsFileName))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .Then(x => logger.Dispose())\n            .Then(x => ThenMessagesAreLogged(notAllowedMessageTypes, allowedMessageTypes))\n            .BDDfy();\n    }\n\n    private Task<int> GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appsettingsFileName)\n        => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot,\n            async app =>\n            {\n                app.Use(async (context, next) =>\n                {\n                    var loggerFactory = context.RequestServices.GetService<IOcelotLoggerFactory>();\n                    var ocelotLogger = loggerFactory.CreateLogger<LogLevelTests>();\n                    ocelotLogger.LogDebug(() => $\"DEBUG: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogTrace(() => $\"TRACE: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogInformation(() =>\n                        $\"INFORMATION: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogWarning(() => $\"WARNING: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogError(() => $\"ERROR: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n                    ocelotLogger.LogCritical(() => $\"CRITICAL: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n\n                    await next.Invoke();\n                });\n                await app.UseOcelot();\n            },\n            null,\n            host => host.ConfigureLogging(l => l.ClearProviders().AddSerilog(logger)),\n            null, null);\n\n    [Fact]\n    public void If_minimum_log_level_is_critical_then_only_critical_messages_are_logged() => TestFactory(\n        [ \"TRACE\", \"INFORMATION\", \"WARNING\", \"ERROR\" ],\n        [ \"CRITICAL\" ], LogLevel.Critical);\n\n    [Fact]\n    public void If_minimum_log_level_is_error_then_critical_and_error_are_logged() => TestFactory(\n        [ \"TRACE\", \"INFORMATION\", \"WARNING\", \"DEBUG\" ],\n        [ \"CRITICAL\", \"ERROR\" ], LogLevel.Error);\n\n    [Fact]\n    public void If_minimum_log_level_is_warning_then_critical_error_and_warning_are_logged() => TestFactory(\n        [ \"TRACE\", \"INFORMATION\", \"DEBUG\" ],\n        [ \"CRITICAL\", \"ERROR\", \"WARNING\" ], LogLevel.Warning);\n    \n    [Fact]\n    public void If_minimum_log_level_is_information_then_critical_error_warning_and_information_are_logged() => TestFactory(\n        [ \"TRACE\", \"DEBUG\" ],\n        [ \"CRITICAL\", \"ERROR\", \"WARNING\", \"INFORMATION\" ], LogLevel.Information);\n\n    [Fact]\n    public void If_minimum_log_level_is_debug_then_critical_error_warning_information_and_debug_are_logged() => TestFactory(\n        [ \"TRACE\" ],\n        [ \"DEBUG\", \"CRITICAL\", \"ERROR\", \"WARNING\", \"INFORMATION\" ], LogLevel.Debug);\n\n    [Fact]  \n    public void If_minimum_log_level_is_trace_then_critical_error_warning_information_debug_and_trace_are_logged() => TestFactory(\n        [],\n        [ \"TRACE\", \"DEBUG\", \"CRITICAL\", \"ERROR\", \"WARNING\", \"INFORMATION\" ], LogLevel.Trace);\n\n    private Logger GetLogger(LogLevel logLevel)\n    {\n        var logFilePath = GetLogFilePath();\n        UpdateAppSettings(logLevel);\n        var logger = logLevel switch\n        {\n            LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.Debug => new LoggerConfiguration().MinimumLevel.Debug()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            LogLevel.None => new LoggerConfiguration()\n                .WriteTo.File(logFilePath)\n                .CreateLogger(),\n            _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null),\n        };\n        return logger;\n    }\n\n    private void UpdateAppSettings(LogLevel logLevel)\n    {\n        var appSettingsFilePath = Path.Combine(AppContext.BaseDirectory, _appSettingsFileName);\n        if (File.Exists(appSettingsFilePath))\n        {\n            File.Delete(appSettingsFilePath);\n        }\n\n        var appSettings = string.Format(AppSettingsFormat, Enum.GetName(typeof(LogLevel), logLevel));\n        File.WriteAllText(appSettingsFilePath, appSettings);\n    }\n\n    private string GetLogFilePath()\n    {\n        var logFilePath = Path.Combine(AppContext.BaseDirectory, _logFileName);\n        return logFilePath;\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            context.Response.StatusCode = 200;\n            return context.Response.WriteAsync(string.Empty);\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Logging/MemoryLogger.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System.Collections.Concurrent;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.Logging;\n\npublic class MemoryLogger : ILogger\n{\n    public readonly ConcurrentQueue<string> _messages = new();\n    public readonly ConcurrentQueue<Exception> _exceptions = new();\n\n    public IReadOnlyCollection<string> Messages => _messages;\n    public IReadOnlyCollection<Exception> Exceptions => _exceptions;\n    public string Logbook => string.Join(Environment.NewLine, _messages);\n\n    public IDisposable BeginScope<TState>(TState state)\n        where TState : notnull => null;\n\n    public bool IsEnabled(LogLevel logLevel) => true;\n\n    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)\n    {\n        if (state is null)\n            return;\n\n        var message = formatter?.Invoke(state, exception);\n        if (message == null)\n            return;\n\n        if (exception is not null)\n        {\n            var builder = new StringBuilder()\n                .AppendLine(message)\n                .Append(exception.ToString());\n            _messages.Enqueue(builder.ToString());\n            _exceptions.Enqueue(exception);\n        }\n        else\n        {\n            _messages.Enqueue(message);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Logging/TestLoggerFactory.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\n\nnamespace Ocelot.AcceptanceTests.Logging;\n\npublic class TestLoggerFactory<TConsumer> : IOcelotLoggerFactory\n{\n    private readonly ILoggerFactory _factory;\n    private readonly IRequestScopedDataRepository _repository;\n    private readonly MemoryLogger _logger;\n    private readonly OcelotLogger _ologger;\n    private bool _disposed;\n\n    public TestLoggerFactory(ILoggerFactory factory, IRequestScopedDataRepository repository)\n    {\n        _factory = factory;\n        _repository = repository;\n        _logger = new();\n        _ologger = new(_logger, _repository);\n    }\n\n    public MemoryLogger Logger => _logger;\n    public IOcelotLogger CreateLogger<TActualConsumer>() => _ologger;\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n            return;\n\n        if (disposing)\n        {\n            _factory?.Dispose();\n        }\n\n        _disposed = true;\n    }\n\n    ~TestLoggerFactory() => Dispose(false);\n\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Metadata;\nusing Ocelot.Middleware;\nusing System.Globalization;\n\nnamespace Ocelot.AcceptanceTests.Metadata;\n\n[Trait(\"Feat\", \"738\")]\npublic sealed class DownstreamMetadataTests : Steps\n{\n    public enum StringArrayConfig\n    {\n        Default = 1,\n        AlternateSeparators,\n        AlternateTrimChars,\n        AlternateStringSplitOptions,\n        Mix,\n    }\n\n    public enum NumberConfig\n    {\n        Default = 1,\n        AlternateNumberStyle,\n        AlternateCulture,\n    }\n\n    public DownstreamMetadataTests()\n    {\n    }\n\n    [Theory]\n    [InlineData(typeof(StringDownStreamMetadataHandler))]\n    [InlineData(typeof(StringArrayDownStreamMetadataHandler))]\n    [InlineData(typeof(BoolDownStreamMetadataHandler))]\n    [InlineData(typeof(DoubleDownStreamMetadataHandler))]\n    [InlineData(typeof(SuperDataContainerDownStreamMetadataHandler))]\n    public void ShouldMatchTargetObjects(Type currentType)\n    {\n        (Dictionary<string, string> sourceDictionary, Dictionary<string, object> _) =\n            GetSourceAndTargetDictionary(currentType);\n\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/\",\n                    DownstreamHostAndPorts = [ Localhost(port) ],\n                    DownstreamScheme = \"http\",\n                    UpstreamPathTemplate = \"/\",\n                    UpstreamHttpMethod = [\"Get\"],\n                    Metadata = sourceDictionary,\n                    DelegatingHandlers = [ currentType.Name ],\n                },\n            },\n        };\n\n        this.Given(x => handler.GivenThereIsAServiceRunningOn(port, MapOK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(currentType))\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    /// <summary>\n    /// Testing the string array type with different configurations.\n    /// </summary>\n    /// <param name=\"separators\">The possible separators.</param>\n    /// <param name=\"trimChars\">The trimmed characters.</param>\n    /// <param name=\"stringSplitOption\">If the empty entries should be removed.</param>\n    /// <param name=\"currentConfig\">The current test configuration.</param>\n    [Theory]\n    [InlineData(new[] { \",\" }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.Default)]\n    [InlineData(\n        new[] { \";\", \".\", \",\" },\n        new[] { ' ' },\n        nameof(StringSplitOptions.None),\n        StringArrayConfig.AlternateSeparators)]\n    [InlineData(\n        new[] { \",\" },\n        new[] { ' ', ';', ':' },\n        nameof(StringSplitOptions.None),\n        StringArrayConfig.AlternateTrimChars)]\n    [InlineData(\n        new[] { \",\" },\n        new[] { ' ' },\n        nameof(StringSplitOptions.RemoveEmptyEntries),\n        StringArrayConfig.AlternateStringSplitOptions)]\n    [InlineData(\n        new[] { \";\", \".\", \",\" },\n        new[] { ' ', '_', ':' },\n        nameof(StringSplitOptions.RemoveEmptyEntries),\n        StringArrayConfig.Mix)]\n    public void ShouldMatchTargetStringArrayAccordingToConfiguration(\n        string[] separators,\n        char[] trimChars,\n        string stringSplitOption,\n        StringArrayConfig currentConfig)\n    {\n        (Dictionary<string, string> sourceDictionary, Dictionary<string, string[]> _) =\n            GetSourceAndTargetDictionariesForStringArrayType(currentConfig);\n\n        sourceDictionary.Add(nameof(StringArrayConfig), currentConfig.ToString());\n\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/\",\n                    DownstreamHostAndPorts = [ Localhost(port) ],\n                    DownstreamScheme = \"http\",\n                    UpstreamPathTemplate = \"/\",\n                    UpstreamHttpMethod = [\"Get\"],\n                    Metadata = sourceDictionary,\n                    DelegatingHandlers = [ nameof(StringArrayDownStreamMetadataHandler) ],\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                MetadataOptions = new()\n                {\n                    Separators = separators,\n                    TrimChars = trimChars,\n                    StringSplitOption = stringSplitOption,\n                },\n            },\n        };\n\n        this.Given(x => handler.GivenThereIsAServiceRunningOn(port, MapOK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(StringArrayDownStreamMetadataHandler)))\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Theory]\n    [InlineData(NumberStyles.Any, \"de-CH\", NumberConfig.Default)]\n    [InlineData(NumberStyles.AllowParentheses | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowLeadingSign, \"de-CH\", NumberConfig.AlternateNumberStyle)]\n    public void ShouldMatchTargetNumberAccordingToConfiguration(\n        NumberStyles numberStyles,\n        string cultureName,\n        NumberConfig currentConfig)\n    {\n        (Dictionary<string, string> sourceDictionary, Dictionary<string, int> _) =\n            GetSourceAndTargetDictionariesForNumberType();\n\n        sourceDictionary.Add(nameof(NumberConfig), currentConfig.ToString());\n\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/\",\n                    DownstreamHostAndPorts = [ Localhost(port) ],\n                    DownstreamScheme = \"http\",\n                    UpstreamPathTemplate = \"/\",\n                    UpstreamHttpMethod = [\"Get\"],\n                    Metadata = sourceDictionary,\n                    DelegatingHandlers = [nameof(IntDownStreamMetadataHandler)],\n                },\n            },\n            GlobalConfiguration = new()\n            {\n                MetadataOptions = new()\n                {\n                    NumberStyle = numberStyles.ToString(),\n                    CurrentCulture = cultureName,\n                },\n            },\n        };\n        GivenThereIsAServiceRunningOn(port);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(IntDownStreamMetadataHandler)))\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    /// <summary>\n    /// Starting ocelot with the delegating handler of type currentType.\n    /// </summary>\n    /// <param name=\"currentType\">The current delegating handler type.</param>\n    /// <exception cref=\"NotImplementedException\">Throws if delegating handler type doesn't match.</exception>\n    private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType)\n    {\n        switch (currentType)\n        {\n            case { } t when t == typeof(StringDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<StringDownStreamMetadataHandler>();\n                break;\n            case { } t when t == typeof(StringArrayDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<StringArrayDownStreamMetadataHandler>();\n                break;\n            case { } t when t == typeof(BoolDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<BoolDownStreamMetadataHandler>();\n                break;\n            case { } t when t == typeof(DoubleDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<DoubleDownStreamMetadataHandler>();\n                break;\n            case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<SuperDataContainerDownStreamMetadataHandler>();\n                break;\n            case { } t when t == typeof(IntDownStreamMetadataHandler):\n                GivenOcelotIsRunningWithDelegatingHandler<IntDownStreamMetadataHandler>();\n                break;\n            default:\n                throw new NotImplementedException();\n        }\n    }\n\n    // It would have been better to use a generic method, but it is not possible to use a generic type as a parameter\n    // for the delegating handler name\n    private class StringDownStreamMetadataHandler : DownstreamMetadataHandler<string>\n    {\n        public StringDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)\n        {\n        }\n    }\n\n    private class StringArrayDownStreamMetadataHandler : DownstreamMetadataHandler<string[]>\n    {\n        public StringArrayDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(\n            httpContextAccessor)\n        {\n        }\n    }\n\n    private class BoolDownStreamMetadataHandler : DownstreamMetadataHandler<bool?>\n    {\n        public BoolDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)\n        {\n        }\n    }\n\n    private class DoubleDownStreamMetadataHandler : DownstreamMetadataHandler<double>\n    {\n        public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)\n        {\n        }\n    }\n\n    private class IntDownStreamMetadataHandler : DownstreamMetadataHandler<int>\n    {\n        public IntDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)\n        {\n        }\n    }\n\n    private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler<SuperDataContainer>\n    {\n        public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(\n            httpContextAccessor)\n        {\n        }\n    }\n\n    /// <summary>\n    /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route\n    /// and checking if the extension method GetMetadata returns the correct value.\n    /// </summary>\n    /// <typeparam name=\"T\">The current type.</typeparam>\n    private class DownstreamMetadataHandler<T> : DelegatingHandler\n    {\n        private readonly IHttpContextAccessor _httpContextAccessor;\n\n        public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor)\n        {\n            _httpContextAccessor = httpContextAccessor;\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(\n            HttpRequestMessage request,\n            CancellationToken cancellationToken)\n        {\n            var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute();\n\n            if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(StringArrayConfig)))\n            {\n                var currentConfig =\n                    Enum.Parse<StringArrayConfig>(downstreamRoute.MetadataOptions.Metadata[nameof(StringArrayConfig)]);\n                downstreamRoute.MetadataOptions.Metadata.Remove(nameof(StringArrayConfig));\n\n                (Dictionary<string, string> _, Dictionary<string, string[]> targetDictionary) =\n                    GetSourceAndTargetDictionariesForStringArrayType(currentConfig);\n\n                foreach (var key in targetDictionary.Keys)\n                {\n                    Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata<string[]>(key));\n                }\n            }\n            else if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(NumberConfig)))\n            {\n                downstreamRoute.MetadataOptions.Metadata.Remove(nameof(NumberConfig));\n\n                (Dictionary<string, string> _, Dictionary<string, int> targetDictionary) =\n                    GetSourceAndTargetDictionariesForNumberType();\n\n                foreach (var key in targetDictionary.Keys)\n                {\n                    Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata<double>(key));\n                }\n            }\n            else\n            {\n                (Dictionary<string, string> _, Dictionary<string, object> targetDictionary) =\n                    GetSourceAndTargetDictionary(typeof(T));\n\n                foreach (var key in targetDictionary.Keys)\n                {\n                    Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata<T>(key));\n                }\n            }\n\n            return base.SendAsync(request, cancellationToken);\n        }\n    }\n\n    public static (Dictionary<string, string> SourceDictionary, Dictionary<string, string[]> TargetDictionary)\n        GetSourceAndTargetDictionariesForStringArrayType(StringArrayConfig currentConfig)\n    {\n        Dictionary<string, string> sourceDictionary;\n        Dictionary<string, string[]> targetDictionary;\n\n        if (currentConfig == StringArrayConfig.Default)\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1, Value2, Value3\" },\n                { \"Key2\", \"Value2, Value3, Value4\" },\n                { \"Key3\", \"Value3, ,Value4, Value5\" },\n            };\n\n            targetDictionary = new Dictionary<string, string[]>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentConfig == StringArrayConfig.AlternateSeparators)\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1; Value2. Value3\" },\n                { \"Key2\", \"Value2. Value3, Value4\" },\n                { \"Key3\", \"Value3, ,Value4; Value5\" },\n            };\n\n            targetDictionary = new Dictionary<string, string[]>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentConfig == StringArrayConfig.AlternateTrimChars)\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1; :, Value2 :, Value3 \" },\n                { \"Key2\", \" Value2, Value3; , Value4\" },\n                { \"Key3\", \"Value3 , ,Value4, Value5 \" },\n            };\n\n            targetDictionary = new Dictionary<string, string[]>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentConfig == StringArrayConfig.AlternateStringSplitOptions)\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1, ,Value2, Value3, \" },\n                { \"Key2\", \"Value2, , ,Value3, Value4, , ,\" },\n                { \"Key3\", \"Value3, ,Value4, , ,Value5\" },\n            };\n\n            targetDictionary = new Dictionary<string, string[]>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentConfig == StringArrayConfig.Mix)\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1; :, Value2. :, Value3 \" },\n                { \"Key2\", \" Value2_, , , Value3; , Value4\" },\n                { \"Key3\", \"Value3:; , ,Value4, Value5 \" },\n            };\n\n            targetDictionary = new Dictionary<string, string[]>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        throw new NotImplementedException();\n    }\n\n    public static (Dictionary<string, string> SourceDictionary, Dictionary<string, int> TargetDictionary)\n        GetSourceAndTargetDictionariesForNumberType()\n    {\n        return (\n            new Dictionary<string, string>\n            {\n                { \"Key1\", \"-2\" }, { \"Key2\", \" (1000000) \" }, { \"Key3\", \"-1000000000  \" },\n            },\n            new Dictionary<string, int> { { \"Key1\", -2 }, { \"Key2\", -1000000 }, { \"Key3\", -1000000000 } });\n    }\n\n    /// <summary>\n    /// Method retrieving the source and target dictionary for the current type.\n    /// The source value is of type string and the target is of type object.\n    /// </summary>\n    /// <param name=\"currentType\">The current type.</param>\n    /// <returns>A source and a target directory to compare the results.</returns>\n    /// <exception cref=\"NotImplementedException\">Throws if type not found.</exception>\n    public static (Dictionary<string, string> SourceDictionary, Dictionary<string, object> TargetDictionary)\n        GetSourceAndTargetDictionary(Type currentType)\n    {\n        Dictionary<string, string> sourceDictionary;\n        Dictionary<string, object> targetDictionary;\n        if (currentType == typeof(StringDownStreamMetadataHandler) || currentType == typeof(string))\n        {\n            sourceDictionary = new Dictionary<string, string> { { \"Key1\", \"Value1\" }, { \"Key2\", \"Value2\" }, };\n\n            targetDictionary = new Dictionary<string, object> { { \"Key1\", \"Value1\" }, { \"Key2\", \"Value2\" }, };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentType == typeof(StringArrayDownStreamMetadataHandler) || currentType == typeof(string[]))\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"Value1, Value2, Value3\" },\n                { \"Key2\", \"Value2, Value3, Value4\" },\n                { \"Key3\", \"Value3, ,Value4, Value5\" },\n            };\n\n            targetDictionary = new Dictionary<string, object>\n            {\n                { \"Key1\", new[] { \"Value1\", \"Value2\", \"Value3\" } },\n                { \"Key2\", new[] { \"Value2\", \"Value3\", \"Value4\" } },\n                { \"Key3\", new[] { \"Value3\", \"Value4\", \"Value5\" } },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentType == typeof(BoolDownStreamMetadataHandler) || currentType == typeof(bool?))\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"true\" },\n                { \"Key2\", \"false\" },\n                { \"Key3\", \"null\" },\n                { \"Key4\", \"disabled\" },\n                { \"Key5\", \"0\" },\n                { \"Key6\", \"1\" },\n                { \"Key7\", \"yes\" },\n                { \"Key8\", \"enabled\" },\n                { \"Key9\", \"on\" },\n                { \"Key10\", \"off\" },\n                { \"Key11\", \"test\" },\n            };\n\n            targetDictionary = new Dictionary<string, object>\n            {\n                { \"Key1\", true },\n                { \"Key2\", false },\n                { \"Key3\", null },\n                { \"Key4\", false },\n                { \"Key5\", false },\n                { \"Key6\", true },\n                { \"Key7\", true },\n                { \"Key8\", true },\n                { \"Key9\", true },\n                { \"Key10\", false },\n                { \"Key11\", null },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentType == typeof(DoubleDownStreamMetadataHandler) || currentType == typeof(double))\n        {\n            sourceDictionary = new Dictionary<string, string> { { \"Key1\", \"0.00001\" }, { \"Key2\", \"0.00000001\" }, };\n\n            targetDictionary = new Dictionary<string, object> { { \"Key1\", 0.00001 }, { \"Key2\", 0.00000001 }, };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler) ||\n            currentType == typeof(SuperDataContainer))\n        {\n            sourceDictionary = new Dictionary<string, string>\n            {\n                { \"Key1\", \"{\\\"key1\\\":\\\"Bonjour\\\",\\\"key2\\\":\\\"Hello\\\",\\\"key3\\\":0.00001,\\\"key4\\\":true}\" },\n            };\n\n            targetDictionary = new Dictionary<string, object>\n            {\n                {\n                    \"Key1\", new SuperDataContainer\n                    {\n                        Key1 = \"Bonjour\", Key2 = \"Hello\", Key3 = 0.00001, Key4 = true,\n                    }\n                },\n            };\n\n            return (sourceDictionary, targetDictionary);\n        }\n\n        throw new NotImplementedException();\n    }\n\n    public class SuperDataContainer\n    {\n        public string Key1 { get; set; }\n\n        public string Key2 { get; set; }\n\n        public double Key3 { get; set; }\n\n        public bool? Key4 { get; set; }\n\n        public override bool Equals(object obj)\n        {\n            // Check for null and compare run-time types.\n            if (obj == null || this.GetType() != obj.GetType())\n            {\n                return false;\n            }\n\n            SuperDataContainer other = (SuperDataContainer)obj;\n            return Key1 == other.Key1 && Key2 == other.Key2 && Key3.Equals(other.Key3) && Key4 == other.Key4;\n        }\n\n        // https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode\n        public override int GetHashCode()\n        {\n            unchecked\n            {\n                int hash = 17;\n                hash = (hash * 23) + (Key1?.GetHashCode() ?? 0);\n                hash = (hash * 23) + (Key2?.GetHashCode() ?? 0);\n                hash = (hash * 23) + Key3.GetHashCode();\n                hash = (hash * 23) + (Key4?.GetHashCode() ?? 0);\n                return hash;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <AssemblyName>Ocelot.AcceptanceTests</AssemblyName>\n    <OutputType>Exe</OutputType>\n    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <NoWarn>$(NoWarn);CS0618;CS1591</NoWarn>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Update=\"appsettings.product.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"appsettings.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"mycert.pfx\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"mycert2.pfx\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Consul\\Ocelot.Provider.Consul.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Eureka\\Ocelot.Provider.Eureka.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Kubernetes\\Ocelot.Provider.Kubernetes.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Polly\\Ocelot.Provider.Polly.csproj\" />\n    <ProjectReference Include=\"..\\..\\testing\\Ocelot.Testing.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"xunit.v3\" Version=\"3.2.2\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"8.0.0\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Consul\" Version=\"1.7.14.10\" />\n    <PackageReference Include=\"Serilog\" Version=\"4.3.1\" />\n    <PackageReference Include=\"Serilog.AspNetCore\" Version=\"10.0.0\" />\n    <PackageReference Include=\"Steeltoe.Discovery.ClientCore\" Version=\"3.3.0\" /><!-- TODO Requires upgrade to v4.0 after package upgraded -->\n    <PackageReference Include=\"TestStack.BDDfy\" Version=\"8.0.9.120-beta\" /><!-- TestStack.BDDfy.Xunit TODO Requires deprecation in favor of Reqnroll, see task 2341 -->\n    <!-- Microsoft -->\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.FileExtensions\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Console\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Debug\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.3.0\" />\n    <PackageReference Include=\"System.IdentityModel.Tokens.Jwt\" Version=\"8.16.0\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 8.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net8.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"8.0.25\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"8.0.25\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 9.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net9.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"9.0.14\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"9.0.14\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 10.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net10.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"10.0.5\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Properties/AssemblyInfo.cs",
    "content": "﻿using System.Reflection;\r\nusing System.Runtime.InteropServices;\r\n\r\n// General Information about an assembly is controlled through the following set of attributes.\r\n// Change these attribute values to modify the information associated with an assembly.\r\n[assembly: AssemblyCompany(\"Three Mammals\")]\r\n[assembly: AssemblyCopyright(\"© 2026 Three Mammals. MIT licensed OSS.\")]\r\n[assembly: AssemblyProduct(\"Ocelot Gateway\")]\r\n[assembly: AssemblyTrademark(\"Ocelot\")]\r\n\r\n// Setting ComVisible to false makes the types in this assembly not visible to COM components.\r\n// If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type.\r\n[assembly: ComVisible(false)]\r\n\r\n// The following GUID is for the ID of the typelib if this project is exposed to COM\r\n[assembly: Guid(\"f8c224fe-36be-45f5-9b0e-666d8f4a9b52\")]\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Properties/BddfyConfig.cs",
    "content": "﻿using System.Collections.Concurrent;\nusing TestStack.BDDfy.Configuration;\n\nnamespace Ocelot.AcceptanceTests.Properties;\n\npublic static class BddfyConfig\n{\n    public static void Configure()\n    {\n        //// Configurator.Processors.ConsoleReport.RunsOn(story => story.Result != Result.Passed);\n        //Configurator.Processors.ConsoleReport.Disable();\n        //Configurator.Processors.Add(() => new BddfyProcessor());\n\n        ////Configurator.BatchProcessors.Add(new BddfyBatchProcessingReporter());\n        //Configurator.BatchProcessors.HtmlReport.Disable();\n    }\n}\n\npublic class BddfyProcessor : IProcessor\n{\n    private static readonly ConcurrentDictionary<string, Scenario> Cache = new();\n    public ProcessType ProcessType => ProcessType.Report;\n    public void Process(Story story)\n    {\n        //Console.WriteLine($\"{story.Result} Story: {story.Namespace} | Total Scenarios: {story.Scenarios.Count()}\");\n        foreach (var scenario in story.Scenarios)\n        {\n            if (Cache.TryAdd(scenario.Id, scenario))\n            {\n                Console.ForegroundColor = scenario.Result == Result.Passed ? ConsoleColor.Green : ConsoleColor.Red;\n                Console.Write(scenario.Result);\n                Console.ForegroundColor = ConsoleColor.Yellow;\n                Console.Write($\" {scenario.Id}: \");\n                Console.ForegroundColor = ConsoleColor.Blue;\n                Console.Write(scenario.Title);\n                Console.ForegroundColor = ConsoleColor.Yellow;\n                Console.WriteLine($\", in {scenario.Duration.TotalSeconds} sec\");\n                Console.ResetColor();\n            }\n        }\n    }\n}\n\npublic class BddfyBatchProcessingReporter : IBatchProcessor\n{\n    private static int totalStories;\n    private static int totalScenarios;\n    private static Result final = Result.NotExecuted;\n\n    public static void Process(Story story)\n    {\n        //foreach (var scenario in story.Scenarios)\n        //{\n        //    //Console.WriteLine($\"Scenario: {scenario.Title} - Status: {scenario.Result}\");\n        //    totalScenarios++;\n        //}\n        totalScenarios += story.Scenarios.Count();\n        totalStories++;\n        final = (Result)Math.Max((int)story.Result, (int)final);\n    }\n\n    public void Process(IEnumerable<Story> stories)\n    {\n        var list = stories.ToList();\n        list.ForEach(Process);\n\n        Console.WriteLine(\"Warning: Per-scenario logging has been disabled!\");\n        Console.WriteLine($\"The {nameof(BddfyBatchProcessingReporter)} has processed total {totalStories} stories with total {totalScenarios} scenarios.\");\n        Console.WriteLine($\"Final result: {final}\");\n        Console.WriteLine();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Properties/GlobalSuppressions.cs",
    "content": "﻿// This file is used by Code Analysis to maintain SuppressMessage\n// attributes that are applied to this project.\n// Project-level suppressions either have no target or are given\n// a specific target and scoped to a namespace, type, member, etc.\n\nusing System.Diagnostics.CodeAnalysis;\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/QualityOfService/PollyQoSTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.AcceptanceTests.Configuration;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Polly;\nusing Polly.CircuitBreaker;\nusing Polly.Timeout;\nusing System.Runtime.CompilerServices;\nusing TimeoutStrategy = Ocelot.Provider.Polly.TimeoutStrategy;\n\nnamespace Ocelot.AcceptanceTests.QualityOfService;\n\n[Trait(\"Feat\", \"23\")] // https://github.com/ThreeMammals/Ocelot/issues/23\n[Trait(\"Feat\", \"39\")] // https://github.com/ThreeMammals/Ocelot/pull/39\npublic sealed class PollyQoSTests : PollyQosSteps\n{\n    [Fact]\n    [Trait(\"Feat\", \"318\")] // https://github.com/ThreeMammals/Ocelot/issues/318\n    [Trait(\"PR\", \"319\")] // https://github.com/ThreeMammals/Ocelot/pull/319\n    public async Task Should_not_timeout()\n    {\n        var qos = new QoSOptions()\n        {\n            BreakDuration = 500,\n            MinimumThroughput = 10,\n            FailureRatio = 0.5,\n            SamplingDuration = 5,\n            Timeout = 1000, // !!!\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos, method: HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, timeout: 10); // !!!\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        await WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"318\")] // https://github.com/ThreeMammals/Ocelot/issues/318\n    [Trait(\"PR\", \"319\")] // https://github.com/ThreeMammals/Ocelot/pull/319\n    public async Task Should_timeout()\n    {\n        var qos = new QoSOptions(1000); // timeout\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos, method: HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, timeout: 2100);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        await WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1550\")] // https://github.com/ThreeMammals/Ocelot/issues/1550\n    [Trait(\"Bug\", \"1706\")] // https://github.com/ThreeMammals/Ocelot/issues/1706\n    [Trait(\"PR\", \"1753\")] // https://github.com/ThreeMammals/Ocelot/pull/1753\n    public async Task Should_open_circuit_breaker_after_two_exceptions()\n    {\n        var qos = new QoSOptions(2, 1000)\n        {\n            Timeout = 100_000, // infinite -> actually no timeout\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        for (int i = 0; i < qos.MinimumThroughput.Value; i++)\n        {\n            await WhenIGetUrlOnTheApiGateway(\"/\");\n            ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError);\n        }\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // opened\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Polly status\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2085\")] // https://github.com/ThreeMammals/Ocelot/issues/2085\n    public async Task Should_open_circuit_breaker_for_DefaultBreakDuration()\n    {\n        int cicdMs = IsCiCd() ? 50 : 0;\n        int invalidDuration = CircuitBreakerStrategy.LowBreakDuration; // valid value must be >500ms, exact 500ms is invalid\n        var qos = new QoSOptions(2, invalidDuration)\n        {\n            Timeout = 100_000,\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError);\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // opened\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Polly status\n        await GivenIWaitMilliseconds(CircuitBreakerStrategy.DefaultBreakDuration - 500); // 5000 - 500 = 4500; BreakDuration is not elapsed\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // still opened\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // still opened\n        GivenThereIsABrokenServiceOnline(HttpStatusCode.NotFound);\n        await GivenIWaitMilliseconds(500 + cicdMs); // BreakDuration should elapse now\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // closed, service online\n        ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound); // closed, service online\n        ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.NotFound));\n    }\n\n    /// <summary>\n    /// This test, written by Tom, is based on a <see cref=\"TimeoutRejectedException\"/> object generated by the <see cref=\"TimeoutStrategy\"/>.\n    /// The <see cref=\"TimeoutRejectedException\"/> objects are handled by the <see cref=\"CircuitBreakerStrategyOptions{HttpResponseMessage}.ShouldHandle\"/> predicate, which is part of the default implementation of the <see cref=\"CircuitBreakerStrategy\"/>.\n    /// </summary>\n    /// <returns>A <see cref=\"Task\"/> representing the asynchronous acceptance test.</returns>\n    [Fact]\n    [Trait(\"PR\", \"39\")] // https://github.com/ThreeMammals/Ocelot/pull/39\n    public async Task Should_open_circuit_breaker_then_close()\n    {\n        var qos = new QoSOptions(CircuitBreakerStrategy.LowMinimumThroughput, CircuitBreakerStrategy.LowBreakDuration + 1) // 501\n        {\n            Timeout = 1000, // -> TimeoutRejectedException\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        const int MillisecondsDelay = 2_100;\n        GivenThereIsAPossiblyBrokenServiceRunningOn(port, \"Hello from Laura\", MillisecondsDelay);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, \"Hello from Laura\");\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // repeat same request because min MinimumThroughput is 2\n        await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, \"Hello from Laura\");\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await GivenIWaitMilliseconds(MillisecondsDelay);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, \"Hello from Laura\");\n    }\n\n    [Fact] // [SkippableFact]\n    [Trait(\"PR\", \"39\")] // https://github.com/ThreeMammals/Ocelot/pull/39\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    public async Task Should_open_circuit_breaker_then_close_without_timeout_strategy()\n    {\n        //Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), SkippingOnMacOS);\n        var qos = new QoSOptions(CircuitBreakerStrategy.LowMinimumThroughput, 1000) // 501\n        {\n            Timeout = null, // switch off timeout strategy\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, qos);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        await TestRouteCircuitBreaker([port], route.UpstreamPathTemplate, route.QoSOptions);\n    }\n\n    [Fact] // [SkippableFact]\n    [Trait(\"PR\", \"39\")] // https://github.com/ThreeMammals/Ocelot/pull/39\n    public async Task Open_circuit_should_not_effect_different_route()\n    {\n        // Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), SkippingOnMacOS);\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var qos1 = new QoSOptions(2, CircuitBreakerStrategy.LowBreakDuration + 1) // 501\n        {\n            Timeout = 1000,\n        };\n        var route = GivenRoute(port1, qos1);\n        var route2 = GivenRoute(port2, new(), \"/working\");\n        var configuration = GivenConfiguration(route, route2);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n        const int MillisecondsDelay = 2_100;\n        GivenThereIsAPossiblyBrokenServiceRunningOn(port1, \"Hello from Laura\", MillisecondsDelay);\n        GivenThereIsAServiceRunningOn(port2, HttpStatusCode.OK, 0, \"Hello from Tom\");\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"Hello from Laura\");\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // repeat same request because min MinimumThroughput is 2\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"Hello from Laura\");\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await WhenIGetUrlOnTheApiGateway(\"/working\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"Hello from Tom\");\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await GivenIWaitMilliseconds(3000);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"Hello from Laura\");\n    }\n\n    // TODO: If failed in parallel execution mode, switch to SequentialTests\n    // This issue may arise when transitioning all tests to parallel execution\n    // This test must be sequential because of usage of the static DownstreamRoute.DefaultTimeoutSeconds\n    [Fact]\n    [Trait(\"Bug\", \"1833\")] // https://github.com/ThreeMammals/Ocelot/issues/1833\n    public async Task Should_timeout_per_default_after_90_seconds()\n    {\n        try\n        {\n            DownstreamRoute.DefaultTimeoutSeconds = 3; // override original value\n            var defTimeoutMs = Ms(DownstreamRoute.DefaultTimeoutSeconds);\n            var port = PortFinder.GetRandomPort();\n            var route = GivenRoute(port, new(new FileQoSOptions()));\n            var configuration = GivenConfiguration(route);\n            GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, defTimeoutMs + 500); // 3.5s > 3s -> ServiceUnavailable\n            GivenThereIsAConfiguration(configuration);\n            await GivenOcelotIsRunningWithPolly();\n            await WhenIGetUrlOnTheApiGateway(\"/\");\n            ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 3 secs -> Timeout exception aka request cancellation\n        }\n        finally\n        {\n            DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout;\n        }\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")] // https://github.com/ThreeMammals/Ocelot/pull/2073\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    public async Task HasRouteAndGlobalTimeouts_RouteTimeoutShouldTakePrecedenceOverGlobalTimeout()\n    {\n        const int RouteTimeoutSeconds = 2, GlobalTimeoutSeconds = 4;\n        int serviceTimeoutMs = Ms(Math.Max(RouteTimeoutSeconds, GlobalTimeoutSeconds)) + 500; // total 4.5 sec\n\n        var port = PortFinder.GetRandomPort();\n        var qos = new FileQoSOptions() { TimeoutValue = Ms(RouteTimeoutSeconds) };\n        var route = GivenRoute(port, new(qos));\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = Ms(GlobalTimeoutSeconds) }; // !!!\n\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, serviceTimeoutMs);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        var watcher = await WatchWhenIGetUrlOnTheApiGateway();\n\n        ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), Ms(RouteTimeoutSeconds) + 500); // (2.0, 2.5) s\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        await ThenTheResponseBodyShouldBeAsync(string.Empty);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    public async Task HasGlobalTimeoutOnly_ForAllRoutesGlobalTimeoutShouldTakePrecedenceOverAbsoluteGlobalTimeout()\n    {\n        const int GlobalTimeoutSeconds = 2;\n        int serviceTimeoutMs = Ms(GlobalTimeoutSeconds + 1); // total 3 sec\n        var ports = PortFinder.GetPorts(2);\n        FileRoute route1 = GivenRoute(ports[0], \"/route1\"),\n            route2 = GivenRoute(ports[1], \"/route2\"); // without QoS timeouts\n        var configuration = GivenConfiguration(route1, route2);\n        configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = Ms(GlobalTimeoutSeconds) }; // !!!\n        GivenThereIsAServiceRunningOn(ports[0], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable\n        GivenThereIsAServiceRunningOn(ports[1], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        var watchers = await Task.WhenAll(\n            WatchWhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate),\n            WatchWhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate));\n\n        int globalTimeoutMs = Ms(GlobalTimeoutSeconds);\n        foreach (var watcher in watchers)\n        {\n            ThenTimeoutIsInRange(watcher, globalTimeoutMs, Ms(DownstreamRoute.DefaultTimeoutSeconds)); // (2.0, 90) so assert roughly\n            ThenTimeoutIsInRange(watcher, globalTimeoutMs, globalTimeoutMs + 500); // (2.0, 2.5) so assert precisely\n            ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler\n            await ThenTheResponseBodyShouldBeAsync(string.Empty);\n        }\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")] // https://github.com/ThreeMammals/Ocelot/pull/2081\n    [Trait(\"Feat\", \"2080\")] // https://github.com/ThreeMammals/Ocelot/issues/2080\n    public async Task HasRouteAndGlobalFailureRatios_RouteFailureRatioShouldTakePrecedenceOverGlobalFailureRatio()\n    {\n        const double RouteFailureRatio = 0.50D, GlobalFailureRatio = 0.75D;\n        var qos = new FileQoSOptions()\n        {\n            ExceptionsAllowedBeforeBreaking = 3, // after 3 actions FailureRatio is activated\n            DurationOfBreak = CircuitBreakerStrategy.LowBreakDuration + 1,\n            FailureRatio = RouteFailureRatio, // 50% of requests\n            SamplingDuration = 1_000,\n        };\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, new(qos));\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.QoSOptions = new() { FailureRatio = GlobalFailureRatio }; // !!!\n\n        int count = 0;\n        bool isOK = false;\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, () => 10, () => !isOK && ++count % 2 == 0); // 1 of 2 fails\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 1 -> 0%\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // fail\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 1 failed of 2 -> 50% but failure ratio is ignored because of 2 actions < 3\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 1 failed of 3 -> 33%\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // fail\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 2 failed of 4 -> 50% -> circuit is open now!\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 2 failed of 5 -> 40%, but circuit is already open\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // fail\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 3 failed of 6 -> 50%, but circuit is already open\n        count.ShouldBe(4); // 2 of 4 were failed, and the service was called 4 times\n        isOK = true; // the next requests should be OK\n        int cicdMs = IsCiCd() ? 50 : 0;\n        await GivenIWaitMilliseconds(qos.DurationOfBreak.Value + cicdMs); // breaking period is over, thus, circuit breaker is closed\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // OK but circuit is closed\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // circuit is closed\n        await ThenTheResponseBodyShouldBeAsync(nameof(HasRouteAndGlobalFailureRatios_RouteFailureRatioShouldTakePrecedenceOverGlobalFailureRatio));\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")] // https://github.com/ThreeMammals/Ocelot/pull/2081\n    [Trait(\"Feat\", \"2080\")] // https://github.com/ThreeMammals/Ocelot/issues/2080\n    public async Task HasGlobalFailureRatioOnly_GlobalFailureRatioShouldTakePrecedenceOverPollyDefaultFailureRatio()\n    {\n        const double GlobalFailureRatio = 0.75D; // Polly def FailureRatio is CircuitBreakerStrategy.DefaultFailureRatio -> 0.1 -> 10%\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port); // without failure ratios\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.QoSOptions = new()\n        {\n            ExceptionsAllowedBeforeBreaking = 2, // after 2 actions FailureRatio is activated\n            DurationOfBreak = CircuitBreakerStrategy.LowBreakDuration + 1,\n            FailureRatio = GlobalFailureRatio, // 75% of requests\n            SamplingDuration = 1_000,\n        }; // !!!\n        int count = 0;\n        bool isOK = false;\n        GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, () => 10, () => !isOK && ++count > 2);\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 1 -> 0%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 2 -> 0%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 1 failed of 3 -> 33%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 2 failed of 4 -> 50%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 3 failed of 5 -> 60%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 4 failed of 6 -> 66%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 5 failed of 7 -> 71%\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 6 failed of 8 -> 75% -> circuit is open now!\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n\n        // Assert circuit is open\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 7 failed of 9 -> 77%, but circuit is already open\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 8 failed of 10 -> 80%, but circuit is already open\n        count.ShouldBe(8); // the service was called 8 times of 10 total\n        isOK = true; // the next requests should be OK\n        int cicdMs = IsCiCd() ? 50 : 0;\n        await GivenIWaitMilliseconds(configuration.GlobalConfiguration.QoSOptions.DurationOfBreak.Value + cicdMs); // breaking period is over, thus, circuit breaker is closed\n        await WhenIGetUrlOnTheApiGateway(\"/\"); // OK but circuit is closed\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // circuit is closed\n        await ThenTheResponseBodyShouldBeAsync(nameof(HasGlobalFailureRatioOnly_GlobalFailureRatioShouldTakePrecedenceOverPollyDefaultFailureRatio));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    public async Task ShouldApplyGlobalQosOptions_ForStaticRoutes()\n    {\n        const int GlobalTimeout = 1500;\n        const int RouteExceptions = 2, GlobalExceptions = 3;\n        const int RouteBreakMs = 1000, GlobalBreakMs = 2000;\n        var ports = PortFinder.GetPorts(3);\n        var route1 = GivenRoute(ports[0],\n            options: null, // no opts -> use global opts\n            \"/route1\");\n        var route2 = GivenRoute(ports[1],\n            new QoSOptions(RouteExceptions, RouteBreakMs),\n            \"/route2\");\n        var route3 = GivenRoute(ports[2],\n            new QoSOptions(0, 0) { Timeout = GlobalTimeout }, // disable Circuit Breaker via disallowing of global opts to substitute\n            \"/noCircuitBreaker\");\n        var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection\n        var globalOptions = configuration.GlobalConfiguration.QoSOptions\n            = new(new QoSOptions(GlobalExceptions, GlobalBreakMs));\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        // TODO: Add acceptance steps that are more parallelism-friendly.\n        // The code below failed due to a shared response object being used for sequential steps.\n        //await Task.WhenAll(\n        //    TestRouteCircuitBreaker(route1, 0, globalOptions), // test global scenario\n        //    TestRouteCircuitBreaker(route2, 1), // test route-level scenario\n        //    TestRouteTimeout(route3));\n        await TestRouteCircuitBreaker([ports[0]], route1.UpstreamPathTemplate, globalOptions, 0); // test global scenario\n        await TestRouteCircuitBreaker([ports[1]], route2.UpstreamPathTemplate, route2.QoSOptions, 1); // test route-level scenario\n        await TestRouteTimeout([ports[2]], route3.UpstreamPathTemplate, route3.QoSOptions);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    public async Task ShouldApplyGlobalQosOptions_ForStaticRoutes_WithGroupedOpts()\n    {\n        const int GlobalTimeout = 1500, GlobalExceptions = 3, GlobalBreakMs = 2000;\n        var ports = PortFinder.GetPorts(3);\n\n        // 1st route\n        var route1 = GivenRoute(ports[0],\n            options: null, // no opts -> no QoS at all\n            \"/route1\");\n        route1.Key = null; // 1st route is not in the global group\n\n        // 2nd route\n        var route2 = GivenRoute(ports[1],\n            options: null, // 2nd route opts will be applied from global ones\n            \"/route2\");\n        route2.Key = \"R2\"; // 2nd route is in the group\n\n        // 3rd route\n        var route3 = GivenRoute(ports[2],\n            new QoSOptions(0, 0) { Timeout = GlobalTimeout }, // disable Circuit Breaker via disallowing of global opts to substitute\n            \"/noCircuitBreaker\");\n\n        var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection\n        var globalOptions = configuration.GlobalConfiguration.QoSOptions\n            = new(new QoSOptions(GlobalExceptions, GlobalBreakMs))\n            {\n                RouteKeys = [\"R2\"],\n            };\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningWithPolly();\n\n        await TestRouteCircuitBreaker([ports[0]], route1.UpstreamPathTemplate, route1.QoSOptions, 0); // no QoS scenario\n        GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, 0); // bring 1st service back online\n        await WhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate)\n            .ContinueWith(t => ThenTheResponseShouldBeAsync(HttpStatusCode.OK, \"OK\"));\n        await TestRouteCircuitBreaker([ports[1]], route2.UpstreamPathTemplate, globalOptions, 1); // test global scenario\n        await TestRouteTimeout([route3.DownstreamHostAndPorts[0].Port], route3.UpstreamPathTemplate, route3.QoSOptions);\n    }\n\n    private FileRoute GivenRoute(int port, QoSOptions options, string upstream = null, string method = null)\n    {\n        var route = GivenRoute(port, upstream, upstream);\n        route.UpstreamHttpMethod = [method ?? HttpMethods.Get];\n        route.QoSOptions = options is null ? null : new(options);\n        return route;\n    }\n\n    private Task<int> GivenOcelotIsRunningWithPolly()\n        => GivenOcelotIsRunningAsync(WithPolly);\n    private static void WithPolly(IServiceCollection services)\n        => services.AddOcelot().AddPolly();\n\n    private static Task GivenIWaitMilliseconds(int ms) => GivenIWaitAsync(ms);\n\n    private void GivenThereIsAPossiblyBrokenServiceRunningOn(int port, string responseBody, int millisecondsDelay, int requestNo = 2)\n    {\n        int requestCount = 0;\n        handler.GivenThereIsAServiceRunningOn(port, async context =>\n        {\n            if (requestCount == requestNo)\n            {\n                // In Polly v8:\n                //   MinimumThroughput (exceptions) must be 2 or more\n                //   BreakDuration (ex. DurationOfBreak) must be > 500\n                //   Timeout (ex. TimeoutValue) must be 1000 or more\n                // So, we wait for 2.1 seconds to make sure the circuit is open\n                // BreakDuration * MinimumThroughput + Timeout\n                // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum\n                await Task.Delay(millisecondsDelay); // 2_100\n            }\n\n            requestCount++;\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\n            await context.Response.WriteAsync(responseBody);\n        });\n    }\n\n    protected override void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, int timeout, [CallerMemberName] string response = nameof(PollyQoSTests))\n        => base.GivenThereIsAServiceRunningOn(port, statusCode, timeout, response);\n}\n\npublic class PollyQosSteps : TimeoutTestsBase, IQosSteps, IDisposable\n{\n    private readonly QosSteps steps;\n    public PollyQosSteps() => steps = new(this);\n\n    public override void Dispose()\n    {\n        steps.Dispose();\n        base.Dispose();\n        GC.SuppressFinalize(this);\n    }\n\n    public void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false)\n        => steps.GivenThereIsABrokenServiceOnline(onlineStatusCode, index, length, isDiscovery);\n\n    public void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0)\n        => steps.GivenThereIsABrokenServiceRunningOn(port, brokenStatusCode, index);\n\n    public void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode,\n        Func<int> timeoutStrategy, Func<bool> failingStrategy, [CallerMemberName] string response = null)\n        => steps.GivenThereIsAServiceRunningOn(port, statusCode, timeoutStrategy, failingStrategy, response);\n\n    public Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false)\n        => steps.TestRouteCircuitBreaker(ports, upstreamPath, qos, index, isDiscovery);\n\n    public Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos)\n        => steps.TestRouteTimeout(ports, upstreamPath, qos);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/QualityOfService/QosSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Provider.Polly;\nusing System.Collections.Concurrent;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.QualityOfService;\n\npublic class QosSteps : Steps, IQosSteps\n{\n    private readonly Steps self;\n    public QosSteps(Steps self) => this.self = self;\n\n    public async Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false)\n    {\n        qos ??= new();\n        await handler.ReleasePortAsync(ports)\n            .ContinueWith(t => self.ReleasePortAsync(ports));\n        int count = PollyQoSResiliencePipelineProvider.DefaultServerErrorCodes.Count;\n        HttpStatusCode[] codes = PollyQoSResiliencePipelineProvider.DefaultServerErrorCodes.ToArray();\n        HttpStatusCode nextBadStatus = codes[DateTime.Now.Millisecond % count];\n        for (int i = 0; i < ports.Length; i++)\n        {\n            GivenThereIsABrokenServiceRunningOn(ports[i], nextBadStatus, index);\n        }\n        for (int i = 0; qos.MinimumThroughput.HasValue && i < qos.MinimumThroughput.Value; i++)\n        {\n            nextBadStatus = codes[DateTime.Now.Millisecond % count];\n            GivenThereIsABrokenServiceOnline(nextBadStatus, index, isDiscovery: isDiscovery);\n            await self.WhenIGetUrlOnTheApiGateway(upstreamPath);\n            await self.ThenTheResponseShouldBeAsync(nextBadStatus, nextBadStatus.ToString());\n        }\n        if (qos.MinimumThroughput.HasValue && qos.MinimumThroughput > 0)\n        {\n            GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, index, isDiscovery: isDiscovery);\n            await self.WhenIGetUrlOnTheApiGateway(upstreamPath);\n            self.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Circuit is open\n\n            GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, index, isDiscovery: isDiscovery);\n            int cicdMs = IsCiCd() ? 100 : 0;\n            await GivenIWaitAsync(qos.BreakDuration.Value + cicdMs); // Wait until the circuit is either half-open or closed\n            await self.WhenIGetUrlOnTheApiGateway(upstreamPath);\n            await self.ThenTheResponseShouldBeAsync(HttpStatusCode.OK, \"OK\");\n        }\n    }\n\n    public async Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos)\n    {\n        int counter = 0;\n        bool notFailing() => false;\n        int firstHasTimeout()\n        {\n            int count = Interlocked.Increment(ref counter),\n                timeout = qos.Timeout.Value;\n            return count <= 1 ? timeout + 100 : timeout / 2;\n        }\n        await handler.ReleasePortAsync(ports)\n            .ContinueWith(t => self.ReleasePortAsync(ports));\n        for (int i = 0; i < ports.Length; i++)\n        {\n            GivenThereIsAServiceRunningOn(ports[i], HttpStatusCode.OK, firstHasTimeout, notFailing);\n        }\n        await self.WhenIGetUrlOnTheApiGateway(upstreamPath);\n        self.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // OnTimeout\n        await self.WhenIGetUrlOnTheApiGateway(upstreamPath);\n        await self.ThenTheResponseShouldBeAsync(HttpStatusCode.OK);\n    }\n\n    public Action<int> CounterStrategy { get; set; }\n\n    public void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode,\n        Func<int> timeoutStrategy, Func<bool> failingStrategy, [CallerMemberName] string response = null)\n    {\n        Task MapBodyWithTimeout(HttpContext context)\n        {\n            int delayMs = timeoutStrategy();\n            bool failed = failingStrategy();\n            HttpStatusCode status = failed ? HttpStatusCode.InternalServerError : statusCode;\n            context.Response.StatusCode = (int)status;\n            CounterStrategy?.Invoke(port);\n            return Task.Delay(delayMs)\n                .ContinueWith(t => context.Response.WriteAsync(response));\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapBodyWithTimeout);\n    }\n\n    public ConcurrentDictionary<int, HttpStatusCode> BrokenServiceStatusCode = new();\n    public void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0)\n    {\n        GivenThereIsABrokenServiceOnline(brokenStatusCode, index);\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            var code = BrokenServiceStatusCode[index];\n            context.Response.StatusCode = (int)code;\n            CounterStrategy?.Invoke(port);\n            return context.Response.WriteAsync(code.ToString());\n        });\n    }\n    public void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false)\n    {\n        if (!isDiscovery)\n        {\n            BrokenServiceStatusCode[index] = onlineStatusCode;\n        }\n        else\n        {\n            foreach (var kv in BrokenServiceStatusCode)\n                BrokenServiceStatusCode[kv.Key] = onlineStatusCode;\n        }\n    }\n}\n\npublic interface IQosSteps\n{\n    Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false);\n    Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos);\n    void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode,\n        Func<int> timeoutStrategy, Func<bool> failingStrategy, [CallerMemberName] string response = null);\n    void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0);\n    void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/RateLimiting/ClientHeaderRateLimitingTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Net.Http.Headers;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.RateLimiting;\nusing System.Runtime.InteropServices;\n\nnamespace Ocelot.AcceptanceTests.RateLimiting;\n\npublic sealed class ClientHeaderRateLimitingTests : RateLimitingSteps\n{\n    const int OK = (int)HttpStatusCode.OK;\n    const int TooManyRequests = (int)HttpStatusCode.TooManyRequests;\n    private int _counter;\n\n    public ClientHeaderRateLimitingTests()\n    { }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    public async Task Should_call_with_rate_limiting()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, limit: 3, period: \"1s\", periodTimespan: 1); // -> 3/1s/w1s, so, periods are equal\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1);\n        ThenTheStatusCodeShouldBeOK();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 2);\n        ThenTheStatusCodeShouldBeOK();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1);\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    public async Task Should_wait_for_period_timespan_to_elapse_before_making_next_request()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            downstream: \"/api/ClientRateLimit?count={count}\", upstream: \"/ClientRateLimit/?{count}\",\n            limit: 3, period: \"1s\", periodTimespan: 1); // -> 3/1s/w1s\n        var configuration = GivenConfiguration(route);\n        _counter = 0;\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1);\n        ThenTheStatusCodeShouldBeOK();\n        GivenIWait(50);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 2\n        ThenTheStatusCodeShouldBeOK();\n        GivenIWait(50);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 3\n        ThenTheStatusCodeShouldBeOK();\n        GivenIWait(50);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 4, exceeded with 150ms shift\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        GivenIWait(500); // half of wait window\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 5\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        GivenIWait(500 + 5); // wait window has elapsed\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 6->1\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"4\"); // total 4 OK responses\n    }\n\n    private int _count = 0;\n    private int Count() => ++_count;\n    private string Url() => $\"/ClientRateLimit/?{Count()}\";\n\n    private async Task WhenIGetUrlOnTheApiGatewayMultipleTimes(Func<string> urlDelegate, long times)\n    {\n        for (long i = 0; i < times; i++)\n        {\n            var url = urlDelegate.Invoke();\n            await WhenIGetUrlOnTheApiGatewayMultipleTimes(url, 1);\n        }\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    public async Task Should_call_middleware_with_white_list_client()\n    {\n        const int Limit = 3;\n        const string ClientID = \"ocelotclient1\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, whitelist: [ClientID],\n            limit: Limit, period: \"3s\", periodTimespan: 2); // main period is greater than wait window one\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        var responses = await WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(\"/ClientRateLimit\",\n                            Limit + 1,\n                            route.RateLimitOptions.ClientIdHeader.IfEmpty(configuration.GlobalConfiguration.RateLimitOptions.ClientIdHeader),\n                            ClientID);\n        ThenTheStatusCodeShouldBeOK();\n        responses.Length.ShouldBe(Limit + 1);\n        responses.ShouldAllBe(response => response.StatusCode == HttpStatusCode.OK);\n        var bodies = responses.Select(r => r.Content.ReadAsStringAsync().Result).ToList();\n        bodies.Sum(int.Parse).ShouldBe(10); // n * (n + 1) / 2 -> 4*5/2 -> 20/2\n        bodies.Sort();\n        bodies.ForEach(body => int.Parse(body).ShouldBe(bodies.IndexOf(body) + 1)); // 1, 2, 3, 4\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1590\")]\n    public async Task StatusShouldNotBeEqualTo429_PeriodTimespanValueIsGreaterThanPeriod()\n    {\n        _counter = 0;\n\n        // Bug scenario\n        const string period = \"1s\";\n        const double periodTimespan = /*30*/3; // but decrease 30 to 3 secs, \"no wasting time\" life hack\n        const long limit = 100L;\n\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, \"/api/ClientRateLimit?count={count}\", \"/ClientRateLimit/?{count}\", new(),\n            limit, period, periodTimespan); // bug scenario, adapted\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        // main scenario\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, limit); // 100 times to reach the limit\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(route.RateLimitOptions.Limit.ToString()); // total 100 OK responses\n\n        // extra scenario\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 101st request should fail\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        GivenIWait((int)TimeSpan.FromSeconds(periodTimespan).TotalMilliseconds); // in 3 secs Wait will elapse\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1);\n        ThenTheStatusCodeShouldBeOK();\n        ThenTheResponseBodyShouldBe(\"101\"); // total 101 OK responses\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1305\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task Should_set_ratelimiting_headers_on_response_when_EnableHeaders_set_to(bool enableHeaders)\n    {\n        int port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, limit: 3, period: \"100s\", periodTimespan: 1000.0D); // 3/100s/w1000.00s\n        route.RateLimitOptions.EnableHeaders = enableHeaders;\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1);\n        ThenTheStatusCodeShouldBeOK();\n        ThenRateLimitingHeadersExistInResponse(enableHeaders);\n        ThenTheResponseHeaderExists(HeaderNames.RetryAfter, false);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 2);\n        ThenTheStatusCodeShouldBeOK();\n        ThenRateLimitingHeadersExistInResponse(enableHeaders);\n        ThenTheResponseHeaderExists(HeaderNames.RetryAfter, false);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1);\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        ThenRateLimitingHeadersExistInResponse(false);\n        ThenTheResponseHeaderExists(HeaderNames.RetryAfter, enableHeaders);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"PR\", \"2294\")]\n    public async Task Should_block_unknown_clients_by_writing_warning_to_body_with_503_status()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, limit: 3, period: \"1s\", periodTimespan: 1); // -> 3/1s/w1s\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(\"/ClientRateLimit\", 1, \"bla-bla-header\", \"spy\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable);\n        ThenTheResponseBodyShouldBe(\"Rate limiting client could not be identified for the route '/ClientRateLimit' due to a missing or unknown client ID header required by rule '3/1s/w1s'!\");\n        ThenTheResponseHeaderExists(HeaderNames.RetryAfter).ShouldBe(\"-1\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"1915\")] // https://github.com/ThreeMammals/Ocelot/issues/1915\n    public async Task Should_apply_global_options_when_there_are_no_route_opts()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port);\n        route.RateLimitOptions = null; // !!!\n        var configuration = GivenConfiguration(route);\n        var global = configuration.GlobalConfiguration.RateLimitOptions;\n        global.Limit = 3;\n        global.Period = \"1s\";\n        global.Wait = \"500ms\";\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 3); // 3\n        ThenTheStatusCodeShouldBeOK();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1); // 4, exceeding\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        int halfOfWaitWindow = 250;\n        GivenIWait(halfOfWaitWindow);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1); // 5\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        ThenTheResponseBodyShouldBe(\"Exceeding!\");\n        var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter);\n\n        if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS\n            Assert.True(retryAfter.StartsWith(\"0.1\") || retryAfter.StartsWith(\"0.2\"));\n        else\n            retryAfter.ShouldStartWith(\"0.2\"); // 0.2xx\n\n        //var seconds = double.Parse(retryAfter);\n        //int theRestOfMilliseconds = (int)(1000 * seconds);\n        /* theRestOfMilliseconds.ShouldBeInRange(200, halfOfWaitWindow); */\n        GivenIWait(halfOfWaitWindow); // the end of wait period\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1); // 1, new counting period has started\n        ThenTheStatusCodeShouldBeOK();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")] // https://github.com/ThreeMammals/Ocelot/issues/1229\n    public async Task Should_apply_group_global_options_when_route_opts_has_a_key()\n    {\n        // 1st route\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, upstream: \"/rateUnlimited\");\n        route.RateLimitOptions = null; // 1st route is not limited\n        route.Key = null; // 1st route is not in the global group\n\n        // 2nd route\n        var port2 = PortFinder.GetRandomPort();\n        var route2 = GivenRoute(port2,\n            downstream: \"/api/ClientRateLimit2?count={count}\", upstream: \"/rateLimited/?{count}\");\n        route2.RateLimitOptions = null; // 2nd route opts will be applied from global ones\n        route2.Key = \"R2\"; // 2nd route is in the group\n\n        var configuration = GivenConfiguration(route, route2);\n        var global = configuration.GlobalConfiguration.RateLimitOptions;\n        global.RouteKeys = [\"R2\"];\n        global.Limit = 3;\n        global.Period = \"1s\";\n        global.Wait = \"500ms\";\n\n        GivenThereIsAServiceRunningOn(port);\n        GivenThereIsAServiceRunningOn(port2, \"/api/ClientRateLimit2\", MapOK);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        // Make requests to the 1st unlimited route\n        var responses = await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/rateUnlimited\", (int)global.Limit + 1);\n        ThenTheStatusCodeShouldBeOK();\n        responses.Length.ShouldBe((int)global.Limit + 1);\n        responses.ShouldAllBe(response => response.StatusCode == HttpStatusCode.OK);\n        var bodies = responses.Select(r => r.Content.ReadAsStringAsync().Result).ToList();\n        bodies.ForEach(b => b.ShouldBe(Body()));\n\n        // Make requests to the 2nd rate-limited route\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/rateLimited/\", 3); // 3\n        ThenTheStatusCodeShouldBeOK();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/rateLimited/\", 1); // 4, exceeding\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        int halfOfWaitWindow = 250;\n        GivenIWait(halfOfWaitWindow);\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/rateLimited/\", 1); // 5\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        ThenTheResponseBodyShouldBe(\"Exceeding!\");\n        var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter);\n\n        if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS\n            Assert.True(retryAfter.StartsWith(\"0.1\") || retryAfter.StartsWith(\"0.2\"));\n        else\n            retryAfter.ShouldStartWith(\"0.2\"); // 0.2xx\n\n        var seconds = double.Parse(retryAfter);\n        int theRestOfMilliseconds = (int)(1000 * seconds);\n        /* theRestOfMilliseconds.ShouldBeInRange(200, halfOfWaitWindow); */\n        GivenIWait(halfOfWaitWindow); // the end of wait period\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/rateLimited/\", 1); // 1, new counting period has started\n        ThenTheStatusCodeShouldBeOK();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2294\")]\n    public async Task Should_rate_limit_using_sliding_period_without_wait_period()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port);\n        route.RateLimitOptions = new(route.RateLimitOptions)\n        {\n            Limit = 3,\n            Period = \"1s\",\n            PeriodTimespan = null,\n            Wait = string.Empty, // No wait window -> sliding period in fixed window aka Period is 1s\n        }; // rule -> 3/1s/w0\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOnPath(port, \"/api/ClientRateLimit\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 3);\n        ThenTheStatusCodeShouldBeOK();\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1);\n        ThenTheStatusCodeShouldBe(TooManyRequests);\n        ThenTheResponseBodyShouldBe(\"Exceeding!\");\n        var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter);\n        retryAfter.ShouldStartWith(\"0.9\"); // 0.9xx\n        int theRestOfMilliseconds = (int)(1000 * double.Parse(retryAfter));\n        theRestOfMilliseconds.ShouldBeGreaterThan(900);\n\n        // Mutual behavior arises from test instability, which is sensitive to consumed CPU resources and thread synchronization in CI/CD environments\n        int slidingPeriodEndsInMs = Environment.GetEnvironmentVariable(\"GITHUB_ACTIONS\") == \"true\" // check GitHub CI-CD context\n            ? (int)RateLimitRule.ParseTimespan(route.RateLimitOptions.Period).TotalMilliseconds // not strict requirement for CI-CD, ensure the test is stable\n            : theRestOfMilliseconds; // otherwise it is strict in local dev env, but somethimes the test fails :D\n        GivenIWait(slidingPeriodEndsInMs); // the end of sliding period\n\n        await WhenIGetUrlOnTheApiGatewayMultipleTimes(\"/ClientRateLimit\", 1); // 1, new counting period has started\n        ThenTheStatusCodeShouldBeOK();\n    }\n\n    private void ThenRateLimitingHeadersExistInResponse(bool headersExist)\n    {\n        response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Limit).ShouldBe(headersExist);\n        response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Remaining).ShouldBe(headersExist);\n        response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Reset).ShouldBe(headersExist);\n    }\n\n    protected override Task MapOK(HttpContext context)\n    {\n        int count = Interlocked.Increment(ref _counter); // thread-safe analog of _counter++\n        context.Response.StatusCode = OK;\n        return context.Response.WriteAsync(count.ToString());\n    }\n\n    private FileRoute GivenRoute(int port, string downstream = null, string upstream = null,\n        List<string> whitelist = null, long? limit = null, string period = null, double? periodTimespan = null)\n    {\n        var route = base.GivenRoute(port, upstream ?? \"/ClientRateLimit\", downstream ?? \"/api/ClientRateLimit\");\n        route.RequestIdKey = \"Oc-RequestId\";\n        route.RateLimitOptions = new()\n        {\n            ClientWhitelist = whitelist,\n            Limit = limit ?? 3,\n            Period = period.IfEmpty(\"1s\"),\n            PeriodTimespan = periodTimespan ?? 1D,\n        };\n        return route;\n    }\n\n    public override FileConfiguration GivenConfiguration(params FileRoute[] routes)\n    {\n        var config = base.GivenConfiguration(routes);\n        config.GlobalConfiguration.RateLimitOptions = new()\n        {\n            ClientIdHeader = \"ClientId\",\n            QuotaExceededMessage = \"Exceeding!\",\n            RateLimitCounterPrefix = \"ABC\",\n            HttpStatusCode = TooManyRequests, // 429\n        };\n        config.GlobalConfiguration.RequestIdKey = \"OcelotClientRequest\";\n        return config;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/RateLimiting/RateLimitingSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.AcceptanceTests.RateLimiting;\n\npublic class RateLimitingSteps : Steps\n{\n    public Task<HttpResponseMessage[]> WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times)\n        => WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(url, times);\n\n    public async Task<HttpResponseMessage[]> WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(string url, int times,\n        string clientIdHeader = \"ClientId\", string clientIdHeaderValue = \"ocelotclient1\")\n    {\n        List<Task<HttpResponseMessage>> tasks = new();\n        for (var i = 0; i < times; i++)\n        {\n            var request = new HttpRequestMessage(new(HttpMethods.Get), url);\n            request.Headers.Add(clientIdHeader, clientIdHeaderValue);\n            tasks.Add(ocelotClient.SendAsync(request));\n        }\n        var responses = await Task.WhenAll(tasks);\n        response = responses.Last();\n        return responses;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class ReasonPhraseTests : Steps\r\n{\r\n    public ReasonPhraseTests() { }\r\n\r\n    [Fact]\r\n    public void Should_return_reason_phrase()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", \"some reason\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .And(_ => ThenTheResponseReasonPhraseIs(\"some reason\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, string reasonPhrase)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\r\n        {\r\n            context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = reasonPhrase;\r\n            return context.Response.WriteAsync(\"YOYO!\");\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.Request;\n\n[Trait(\"PR\", \"1972\")]\npublic sealed class RequestMapperTests : Steps\n{\n    public RequestMapperTests()\n    {\n    }\n\n    [Fact]\n    public void Should_map_request_without_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\";;\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_map_request_with_content_length()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(\"This is some content\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"20;;This is some content\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_map_request_with_empty_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StringContent(\"\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"0;;\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_map_request_with_chunked_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new ChunkedContent(\"This \", \"is some content\")))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\";chunked;This is some content\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_map_request_with_empty_chunked_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new ChunkedContent()))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\";chunked;\"))\n            .BDDfy();\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode status)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\n        {\n            var request = context.Request;\n            var response = context.Response;\n            response.StatusCode = (int)status;\n\n            await response.WriteAsync(request.ContentLength + \";\" + request.Headers.TransferEncoding + \";\");\n            await request.Body.CopyToAsync(response.Body);\n        });\n    }\n\n    private static FileRoute GivenRoute(int port, string method = null) => new()\n    {\n        DownstreamPathTemplate = \"/\",\n        DownstreamScheme = Uri.UriSchemeHttp,\n        DownstreamHostAndPorts = new()\n        {\n            new(\"localhost\", port),\n        },\n        UpstreamPathTemplate = \"/\",\n        UpstreamHttpMethod = [method ?? HttpMethods.Get],\n    };\n}\n\ninternal class ChunkedContent : HttpContent\n{\n    private readonly string[] _chunks;\n\n    public ChunkedContent(params string[] chunks)\n    {\n        _chunks = chunks;\n    }\n\n    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)\n    {\n        foreach (var chunk in _chunks)\n        {\n            var bytes = Encoding.Default.GetBytes(chunk);\n            await stream.WriteAsync(bytes, 0, bytes.Length);\n        }\n    }\n\n    protected override bool TryComputeLength(out long length)\n    {\n        length = -1;\n        return false;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Ocelot.Configuration.File;\nusing System.Security.Cryptography;\n\nnamespace Ocelot.AcceptanceTests.Request;\n\n[Trait(\"PR\", \"1972\")] // https://github.com/ThreeMammals/Ocelot/pull/1972\npublic sealed class StreamContentTests : Steps\n{\n#if NET10_0_OR_GREATER\n    [Fact(Skip = \"TODO Require fixing for net10.0 TFM or streaming feature review.\")]\n#else\n    [Fact]\n#endif\n    public void Should_stream_with_content_length()\n    {\n        var contentSize = 1024L * 1024L * 1024L; // 1GB\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StreamTestContent(contentSize, false)))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(contentSize + \";;\" + contentSize))\n            .BDDfy();\n    }\n\n#if NET10_0_OR_GREATER\n    [Fact(Skip = \"TODO Require fixing for net10.0 TFM or streaming feature review.\")]\n#else\n    [Fact]\n#endif\n    public async Task Should_stream_with_chunked_content()\n    {\n        var contentSize = 1024L * 1024L * 1024L; // 1GB\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunningAsync())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new StreamTestContent(contentSize, true)))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\";chunked;\" + contentSize))\n            .BDDfy();\n    }\n\n    public override void GivenThereIsAServiceRunningOn(int port, string basePath)\n    {\n        static void options(KestrelServerOptions o)\n        {\n            o.Limits.MaxRequestBodySize = long.MaxValue;\n        }\n        var baseUrl = DownstreamUrl(port);\n        handler.GivenThereIsAServiceRunningOnWithKestrelOptions(baseUrl, basePath, options, async context =>\n        {\n            var request = context.Request;\n            var response = context.Response;\n\n            long streamLength = 0;\n            int readBytes;\n            var buffer = new byte[8192 - 1]; // Not aligned to sender\n\n            do\n            {\n                readBytes = await request.Body.ReadAsync(buffer, 0, buffer.Length);\n                streamLength += readBytes;\n            } while (readBytes > 0);\n\n            response.StatusCode = 200;\n            await response.WriteAsync(request.ContentLength + \";\" + request.Headers.TransferEncoding + \";\" + streamLength);\n        });\n    }\n\n    private static FileRoute GivenRoute(int port, string method = null) => new()\n    {\n        DownstreamPathTemplate = \"/\",\n        DownstreamScheme = Uri.UriSchemeHttp,\n        DownstreamHostAndPorts = new()\n        {\n            new(\"localhost\", port),\n        },\n        UpstreamPathTemplate = \"/\",\n        UpstreamHttpMethod = [method ?? HttpMethods.Get],\n    };\n}\n\ninternal class StreamTestContent : HttpContent\n{\n    private readonly long _size;\n    private readonly bool _sendChunked;\n    private readonly byte[] _dataBuffer;\n\n    public StreamTestContent(long size, bool sendChunked)\n    {\n        _size = size;\n        _sendChunked = sendChunked;\n        _dataBuffer = RandomNumberGenerator.GetBytes(8192);\n    }\n\n    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)\n    {\n        var remaining = _size;\n        while (remaining > 0)\n        {\n            var count = (int)Math.Min(remaining, _dataBuffer.Length);\n            await stream.WriteAsync(_dataBuffer, 0, count);\n            remaining -= count;\n        }\n    }\n\n    protected override bool TryComputeLength(out long length)\n    {\n        if (_sendChunked)\n        {\n            length = -1;\n            return false;\n        }\n        else\n        {\n            length = _size;\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/RequestIdTests.cs",
    "content": "﻿namespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class RequestIdTests : Steps\r\n{\r\n    public const string RequestIdKey = \"Oc-RequestId\";\r\n\r\n    public RequestIdTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_default_request_id_and_forward()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        route.RequestIdKey = RequestIdKey;\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheRequestIdIsReturned())\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_request_id_and_forward()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        var requestId = Guid.NewGuid().ToString();\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGatewayWithRequestId(\"/\", requestId))\r\n            .Then(x => ThenTheRequestIdIsReturned(requestId))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_global_request_id_and_forward()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        configuration.GlobalConfiguration.RequestIdKey = RequestIdKey;\r\n        var requestId = Guid.NewGuid().ToString();\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGatewayWithRequestId(\"/\", requestId))\r\n            .Then(x => ThenTheRequestIdIsReturned(requestId))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_global_request_id_create_and_forward()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        configuration.GlobalConfiguration.RequestIdKey = RequestIdKey;\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheRequestIdIsReturned())\r\n            .BDDfy();\r\n    }\r\n\r\n    private async Task WhenIGetUrlOnTheApiGatewayWithRequestId(string url, string requestId)\r\n    {\r\n        ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId);\r\n        response = await ocelotClient.GetAsync(url);\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, context =>\r\n        {\r\n            context.Request.Headers.TryGetValue(RequestIdKey, out var requestId);\r\n            context.Response.Headers[RequestIdKey] = requestId.First();\r\n            return Task.CompletedTask;\r\n        });\r\n    }\r\n\r\n    private void ThenTheRequestIdIsReturned()\r\n        => response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty();\r\n    private void ThenTheRequestIdIsReturned(string expected)\r\n        => response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected);\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Requester/MessageInvokerPoolTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Requester;\n\nnamespace Ocelot.AcceptanceTests.Requester;\n\npublic sealed class MessageInvokerPoolTests : RequesterSteps\n{\n    #region TODO Redevelop to minimize the code\n    [Fact]\n    [Trait(\"PR\", \"1824\")] // https://github.com/ThreeMammals/Ocelot/pull/1824\n    public async Task Should_reuse_cookies_from_container()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(new())\n            .WithHttpHandlerOptions(new() { UseCookieContainer = true, UseProxy = true })\n            .WithLoadBalancerKey(string.Empty)\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build())\n\n            // The test should pass without timeout definition -> implicit default timeout\n            //.WithTimeout(DownstreamRoute.DefaultTimeoutSeconds)\n            .Build();\n\n        //using ServiceHandler handler = new();\n        var port = PortFinder.GetRandomPort();\n        GivenADownstreamService(port); // sometimes it fails because of port binding\n\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        GivenARequest(route, port);\n\n        // Act, Assert\n        var toUrl = DownstreamUrl(port);\n        await WhenICallTheClient(toUrl);\n        _response.Headers.TryGetValues(\"Set-Cookie\", out _).ShouldBeTrue();\n\n        // Act, Assert\n        await WhenICallTheClient(toUrl);\n        _response.StatusCode.ShouldBe(HttpStatusCode.OK);\n    }\n    private Mock<IDelegatingHandlerFactory> _handlerFactory;\n    private HttpResponseMessage _response;\n    private MessageInvokerPool _pool;\n    private readonly DefaultHttpContext _context = new();\n    private readonly Mock<IOcelotLogger> _ocelotLogger = new();\n    private readonly Mock<IOcelotLoggerFactory> _ocelotLoggerFactory = new();\n    private async Task WhenICallTheClient(string url)\n    {\n        var messageInvoker = _pool.Get(_context.Items.DownstreamRoute());\n        _response = await messageInvoker\n            .SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None);\n    }\n    private void GivenAMessageInvokerPool() =>\n        _pool = new MessageInvokerPool(_handlerFactory.Object, _ocelotLoggerFactory.Object);\n    private void GivenTheFactoryReturns(List<DelegatingHandler> handlers)\n    {\n        _handlerFactory = new Mock<IDelegatingHandlerFactory>();\n        _handlerFactory.Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(handlers);\n    }\n    private void GivenARequest(DownstreamRoute downstream, int port)\n        => GivenARequestWithAUrlAndMethod(downstream, port, HttpMethod.Get);\n    private void GivenARequestWithAUrlAndMethod(DownstreamRoute downstream, int port, HttpMethod method)\n    {\n        var url = DownstreamUrl(port);\n        _context.Items.UpsertDownstreamRoute(downstream);\n        _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage\n        { RequestUri = new Uri(url), Method = method }));\n    }\n\n    private void GivenADownstreamService(int port)\n    {\n        var count = 0;\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            if (count == 0)\n            {\n                context.Response.Cookies.Append(\"test\", \"0\");\n                context.Response.StatusCode = 200;\n                count++;\n                return Task.CompletedTask;\n            }\n\n            if (count == 1)\n            {\n                if (context.Request.Cookies.TryGetValue(\"test\", out var cookieValue) ||\n                    context.Request.Headers.TryGetValue(\"Set-Cookie\", out var headerValue))\n                {\n                    context.Response.StatusCode = 200;\n                    return Task.CompletedTask;\n                }\n\n                context.Response.StatusCode = 500;\n            }\n\n            return Task.CompletedTask;\n        });\n    }\n    #endregion\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2320\")]\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\n    public async Task ShouldApplyGlobalHttpHandlerOptions_ForStaticRoutes()\n    {\n        var ports = PortFinder.GetPorts(3);\n        var route1 = GivenRoute(ports[0], \"/route1\", null); // no opts -> use global opts\n        var route2 = GivenRoute(ports[1], \"/route2\", GivenOptions(99, 99, useTracing: true));\n        var route3 = GivenRoute(ports[2], \"/noTracing\", GivenOptions());\n        var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection\n        var globalOptions = configuration.GlobalConfiguration.HttpHandlerOptions = new(GivenOptions(100, 100, useTracing: false));\n\n        GivenThereIsAServiceRunningOnPath(ports[0], \"/route1\");\n        GivenThereIsAServiceRunningOnPath(ports[1], \"/route2\");\n        GivenThereIsAServiceRunningOnPath(ports[2], \"/noTracing\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithRequesterTesting);\n\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        await WhenIGetUrlOnTheApiGateway(\"/noTracing\");\n\n        ThenTheResponseBody();\n        ThenRouteHttpHandlerOptionsAre(route1, globalOptions.MaxConnectionsPerServer.Value, globalOptions.PooledConnectionLifetimeSeconds.Value, globalOptions.UseTracing.Value);\n        ThenRouteHttpHandlerOptionsAre(route2, 99, 99, true);\n        ThenRouteHttpHandlerOptionsAre(route3, 100, 100, false);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2320\")]\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\n    public async Task ShouldApplyGlobalGroupHttpHandlerOptions_ForStaticRoutes_WhenRouteOptsHasAKey()\n    {\n        // 1st route\n        var ports = PortFinder.GetPorts(3);\n        var route1 = GivenRoute(ports[0], \"/route1\", null);\n        route1.Key = null; // 1st route is not in the global group\n\n        // 2nd route\n        var route2 = GivenRoute(ports[1], \"/route2\", null); // 2nd route opts will be applied from global ones\n        route2.Key = \"R2\"; // 2nd route is in the group\n\n        // 3rd route\n        var route3 = GivenRoute(ports[2], \"/noTracing\", GivenOptions(88, 88, useTracing: false));\n\n        var configuration = GivenConfiguration(route1, route2, route3);\n        var globalOptions = configuration.GlobalConfiguration.HttpHandlerOptions = new(GivenOptions(100, 100, useTracing: true))\n        {\n            RouteKeys = [\"R2\"],\n        };\n\n        GivenThereIsAServiceRunningOnPath(ports[0], \"/route1\");\n        GivenThereIsAServiceRunningOnPath(ports[1], \"/route2\");\n        GivenThereIsAServiceRunningOnPath(ports[2], \"/noTracing\");\n        GivenThereIsAConfiguration(configuration);\n        await GivenOcelotIsRunningAsync(WithRequesterTesting);\n\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        await WhenIGetUrlOnTheApiGateway(\"/noTracing\");\n\n        ThenTheResponseBody();\n        ThenRouteHttpHandlerOptionsAre(route1, int.MaxValue, HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, false);\n        ThenRouteHttpHandlerOptionsAre(route2, globalOptions.MaxConnectionsPerServer.Value, globalOptions.PooledConnectionLifetimeSeconds.Value, globalOptions.UseTracing.Value);\n        ThenRouteHttpHandlerOptionsAre(route3, 88, 88, false);\n    }\n\n    private void ThenRouteHttpHandlerOptionsAre(FileRoute route, int maxConnections, int seconds, bool useTracing)\n    {\n        var pool = OcelotServices.GetService<IMessageInvokerPool>() as TestMessageInvokerPool;\n        var tracer = OcelotServices.GetService<IOcelotTracer>() as TestTracer;\n        var kv = pool.ShouldNotBeNull()\n            .CreatedHandlers.Single(x => x.Key.UpstreamPathTemplate.OriginalValue == route.UpstreamPathTemplate);\n        var downstream = kv.Key;\n        var httpHandler = kv.Value;\n        httpHandler.MaxConnectionsPerServer.ShouldBe(maxConnections);\n        httpHandler.PooledConnectionLifetime.TotalSeconds.ShouldBe(seconds);\n        downstream.HttpHandlerOptions.UseTracing.ShouldBe(useTracing);\n        var request = tracer.Requests.Keys.SingleOrDefault(k => k.RequestUri.AbsolutePath == route.UpstreamPathTemplate);\n        (request != null).ShouldBe(useTracing);\n    }\n\n    private static FileHttpHandlerOptions GivenOptions(int maxConnections = 100, int pooledConnectionSeconds = 100,\n        bool useCookieContainer = false, bool useProxy = false, bool useTracing = false) => new()\n    {\n        MaxConnectionsPerServer = maxConnections,\n        PooledConnectionLifetimeSeconds = pooledConnectionSeconds,\n        UseCookieContainer = useCookieContainer,\n        UseProxy = useProxy,\n        UseTracing = useTracing,\n    };\n\n    private FileRoute GivenRoute(int port, string path = null, FileHttpHandlerOptions options = null)\n    {\n        var r = GivenRoute(port, path, path);\n        r.HttpHandlerOptions = options;\n        return r;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing System.Runtime.InteropServices;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.Requester;\n\n[Trait(\"Bug\", \"749\")] // https://github.com/ThreeMammals/Ocelot/issues/749\n[Trait(\"PR\", \"1769\")] // https://github.com/ThreeMammals/Ocelot/pull/1769\npublic sealed class PayloadTooLargeTests : Steps\n{\n    private const string Payload =\n        \"[{\\\"_id\\\":\\\"6540f8ee7beff536c1304e3a\\\",\\\"index\\\":0,\\\"guid\\\":\\\"349307e2-5b1b-4ea9-8e42-d0d26b35059e\\\",\\\"isActive\\\":true,\\\"balance\\\":\\\"$2,458.86\\\",\\\"picture\\\":\\\"http://placehold.it/32x32\\\",\\\"age\\\":36,\\\"eyeColor\\\":\\\"blue\\\",\\\"name\\\":\\\"WalshSloan\\\",\\\"gender\\\":\\\"male\\\",\\\"company\\\":\\\"ENOMEN\\\",\\\"email\\\":\\\"walshsloan@enomen.com\\\",\\\"phone\\\":\\\"+1(818)463-2479\\\",\\\"address\\\":\\\"863StoneAvenue,Islandia,NewHampshire,7062\\\",\\\"about\\\":\\\"Exvelitelitutsintlaborisofficialaborisreprehenderittemporsitminim.Exveniamexetesse.Reprehenderitirurealiquipsuntnostrudcillumaliquipsuntvoluptateessenisivoluptatetemporexercitationsint.Laborumexestipsumincididuntvelit.Idnisiproidenttemporelitnonconsequatestnostrudmollit.\\\\r\\\\n\\\",\\\"registered\\\":\\\"2014-11-13T01:53:09-01:00\\\",\\\"latitude\\\":-1.01137,\\\"longitude\\\":160.133312,\\\"tags\\\":[\\\"nisi\\\",\\\"eu\\\",\\\"anim\\\",\\\"ipsum\\\",\\\"fugiat\\\",\\\"excepteur\\\",\\\"culpa\\\"],\\\"friends\\\":[{\\\"id\\\":0,\\\"name\\\":\\\"MayNoel\\\"},{\\\"id\\\":1,\\\"name\\\":\\\"RichardsDiaz\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"JannieHarvey\\\"}],\\\"greeting\\\":\\\"Hello,WalshSloan!Youhave6unreadmessages.\\\",\\\"favoriteFruit\\\":\\\"banana\\\"},{\\\"_id\\\":\\\"6540f8ee39e04d0ac854b05d\\\",\\\"index\\\":1,\\\"guid\\\":\\\"0f210e11-94a1-45c7-84a4-c2bfcbe0bbfb\\\",\\\"isActive\\\":false,\\\"balance\\\":\\\"$3,371.91\\\",\\\"picture\\\":\\\"http://placehold.it/32x32\\\",\\\"age\\\":25,\\\"eyeColor\\\":\\\"green\\\",\\\"name\\\":\\\"FergusonIngram\\\",\\\"gender\\\":\\\"male\\\",\\\"company\\\":\\\"DOGSPA\\\",\\\"email\\\":\\\"fergusoningram@dogspa.com\\\",\\\"phone\\\":\\\"+1(804)599-2376\\\",\\\"address\\\":\\\"130RiverStreet,Bellamy,DistrictOfColumbia,9522\\\",\\\"about\\\":\\\"Duisvoluptatemollitullamcomollitessedolorvelit.Nonpariaturadipisicingsintdoloranimveniammollitdolorlaborumquisnulla.Ametametametnonlaborevoluptate.Eiusmoddocupidatatveniamirureessequiullamcoincididuntea.\\\\r\\\\n\\\",\\\"registered\\\":\\\"2014-11-01T03:51:36-01:00\\\",\\\"latitude\\\":-57.122954,\\\"longitude\\\":-91.22665,\\\"tags\\\":[\\\"nostrud\\\",\\\"ipsum\\\",\\\"id\\\",\\\"cupidatat\\\",\\\"consectetur\\\",\\\"labore\\\",\\\"ullamco\\\"],\\\"friends\\\":[{\\\"id\\\":0,\\\"name\\\":\\\"TabithaHuffman\\\"},{\\\"id\\\":1,\\\"name\\\":\\\"LydiaStark\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"FaithStuart\\\"}],\\\"greeting\\\":\\\"Hello,FergusonIngram!Youhave3unreadmessages.\\\",\\\"favoriteFruit\\\":\\\"banana\\\"}]\";\n\n    [Fact]\n    public void Should_throw_payload_too_large_exception_using_kestrel()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(1024))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload))))\n            .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_throw_payload_too_large_exception_using_http_sys()\n    {\n        Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), \"Test is unstable for all platforms except Windows OS\");\n\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, HttpMethods.Post);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port);\n        this.Given(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(1024))\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload))))\n            .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge))\n            .BDDfy();\n    }\n\n    private static FileRoute GivenRoute(int port, string method = null) => new()\n    {\n        DownstreamPathTemplate = \"/\",\n        DownstreamHostAndPorts = new()\n        {\n            new(\"localhost\", port),\n        },\n        DownstreamScheme = Uri.UriSchemeHttp,\n        UpstreamPathTemplate = \"/\",\n        UpstreamHttpMethod = [method ?? HttpMethods.Get],\n    };\n\n    private Task<int> GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(long customBodyMaxSize)\n        => GivenOcelotHostIsRunning(\n            null, null, null, null,\n            host => host.UseKestrel(options => options.Limits.MaxRequestBodySize = customBodyMaxSize),\n            null, null);\n\n#pragma warning disable IDE0079 // Remove unnecessary suppression\n#pragma warning disable CA1416 // Validate platform compatibility\n    private Task<int> GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(long customBodyMaxSize)\n    {\n        int port = PortFinder.GetRandomPort();\n        var ocelotUrl = DownstreamUrl(port);\n        void ConfigureHttpSys(IWebHostBuilder builder) => builder\n                .ConfigureAppConfiguration(WithBasicConfiguration)\n                .ConfigureServices(WithAddOcelot)\n                .Configure(WithUseOcelot)\n                .UseUrls(ocelotUrl)\n                .UseHttpSys(options => options.MaxRequestBodySize = customBodyMaxSize);\n        return GivenOcelotHostIsRunning(null, null, null, ConfigureHttpSys, null, null,\n            client => client.BaseAddress = new(ocelotUrl));\n    }\n#pragma warning restore CA1416\n#pragma warning restore IDE0079\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Requester/RequesterSteps.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Logging;\nusing Ocelot.Requester;\n\nnamespace Ocelot.AcceptanceTests.Requester;\n\npublic class RequesterSteps : Steps\n{\n    public static void WithRequesterTesting(IServiceCollection services)\n        => WithRequesterTesting(services, true);\n    public static void WithRequesterTesting(IServiceCollection services, bool addOcelot)\n    {\n        if (addOcelot) services.AddOcelot();\n        services\n            .AddSingleton<IOcelotTracer, TestTracer>()\n            .RemoveAll<IMessageInvokerPool>()\n            .AddSingleton<IMessageInvokerPool, TestMessageInvokerPool>();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Requester/TestMessageInvokerPool.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Logging;\nusing Ocelot.Requester;\nusing System.Collections.Concurrent;\n\nnamespace Ocelot.AcceptanceTests.Requester;\n\npublic class TestMessageInvokerPool : MessageInvokerPool, IMessageInvokerPool\n{\n    public TestMessageInvokerPool(IDelegatingHandlerFactory handlerFactory, IOcelotLoggerFactory loggerFactory)\n        : base(handlerFactory, loggerFactory) { }\n\n    public readonly ConcurrentDictionary<DownstreamRoute, SocketsHttpHandler> CreatedHandlers = new();\n    protected override SocketsHttpHandler CreateHandler(DownstreamRoute route)\n    {\n        var handler = base.CreateHandler(route);\n        CreatedHandlers[route] = handler;\n        return handler;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Requester/TestTracer.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing System.Collections.Concurrent;\n\nnamespace Ocelot.AcceptanceTests.Requester;\n\npublic class TestTracer : IOcelotTracer\n{\n    public readonly ConcurrentBag<string> Events = new();\n    public readonly ConcurrentDictionary<HttpRequestMessage, HttpResponseMessage> Requests = new();\n\n    public void Event(HttpContext httpContext, string @event)\n        => Events.Add(@event);\n\n    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Action<string> addTraceIdToRepo, Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> baseSendAsync, CancellationToken cancellationToken)\n    {\n        addTraceIdToRepo?.Invoke(\"12345\");\n        var response = await baseSendAsync.Invoke(request, cancellationToken).ConfigureAwait(false);\n        Requests[request] = response;\n        return response;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ResponseCodeTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class ResponseCodeTests : Steps\r\n{\r\n    public ResponseCodeTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void ShouldReturnResponse304WhenServiceReturns304()\r\n    {\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenCatchAllRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/inline.132.bundle.js\", HttpStatusCode.NotModified))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/inline.132.bundle.js\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotModified))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\n        {\n            context.Response.StatusCode = (int)statusCode;\r\n            return context.Response.WriteAsync(statusCode.ToString());\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.DependencyInjection;\r\nusing Ocelot.Logging;\r\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\r\npublic sealed class ReturnsErrorTests : Steps\r\n{\r\n    [Fact]\r\n    [Trait(\"Feat\", \"603\")] // https://github.com/ThreeMammals/Ocelot/issues/603\r\n    [Trait(\"PR\", \"1149\")] // https://github.com/ThreeMammals/Ocelot/pull/1149\r\n    [Trait(\"Release\", \"15.0.1\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.1\r\n    public void Should_return_bad_gateway_error_if_downstream_service_doesnt_respond()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Commit\", \"1599694\")] // https://github.com/ThreeMammals/Ocelot/commit/159969483b64c5491b1d86b1aa4dac7b4b2a3ba1\r\n    [Trait(\"Commit\", \"ef3deec\")] // https://github.com/ThreeMammals/Ocelot/commit/ef3deec8da78fd282f6b5f2bff8e6d6853496c31\r\n    [Trait(\"Release\", \"1.4.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.0\r\n    public void Should_return_internal_server_error_if_downstream_service_returns_internal_server_error()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"492\")] // https://github.com/ThreeMammals/Ocelot/issues/492\r\n    [Trait(\"PR\", \"1055\")] // https://github.com/ThreeMammals/Ocelot/pull/1055\r\n    [Trait(\"Release\", \"14.0.4\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/14.0.4\r\n    [Trait(\"Commit\", \"9da55ea\")] // https://github.com/ThreeMammals/Ocelot/commit/9da55ea037d0df3b8b22d32dec9b004a50709251\r\n    [Trait(\"PR\", \"1106\")] // https://github.com/ThreeMammals/Ocelot/pull/1106\r\n    public void Should_log_warning_if_downstream_service_returns_internal_server_error()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => x.GivenOcelotIsRunningWithLogger())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenWarningShouldBeLogged(1))\r\n            .BDDfy();\r\n    }\r\n\r\n    private Task<int> GivenOcelotIsRunningWithLogger()\r\n        => GivenOcelotIsRunningAsync(s => s\r\n            .AddOcelot()\r\n            .Services\r\n            .AddSingleton<IOcelotLoggerFactory, MockLoggerFactory>());\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, context => throw new Exception(\"BLAMMMM\"));\r\n    }\r\n\r\n    private void ThenWarningShouldBeLogged(int howMany)\r\n    {\r\n        var loggerFactory = (MockLoggerFactory)OcelotServices.GetService<IOcelotLoggerFactory>();\r\n        loggerFactory.Verify(Times.Exactly(howMany));\r\n    }\r\n\r\n    internal class MockLoggerFactory : IOcelotLoggerFactory\r\n    {\r\n        private Mock<IOcelotLogger> _logger;\r\n        private bool _disposed;\r\n\r\n        public IOcelotLogger CreateLogger<T>()\r\n        {\r\n            if (_disposed)\r\n                return null;\r\n\r\n            if (_logger != null)\r\n                return _logger.Object;\r\n\r\n            _logger = new Mock<IOcelotLogger>();\r\n            _logger.Setup(x => x.LogWarning(It.IsAny<string>())).Verifiable();\r\n            _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>())).Verifiable();\r\n            return _logger.Object;\r\n        }\r\n\r\n        public void Verify(Times howMany)\r\n            => _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), howMany);\r\n\r\n        protected virtual void Dispose(bool disposing)\r\n        {\r\n            if (_disposed)\r\n                return;\r\n\r\n            if (disposing)\r\n            {\r\n                _logger = null;\r\n            }\r\n\r\n            _logger = null;\r\n            _disposed = true;\r\n        }\r\n\r\n        public void Dispose()\r\n        {\r\n            Dispose(disposing: true);\r\n            GC.SuppressFinalize(this);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.AcceptanceTests.Routing;\n\n[Trait(\"PR\", \"1312\")]\n[Trait(\"Feat\", \"360\")]\npublic sealed class RoutingBasedOnHeadersTests : Steps\n{\n    private string _downstreamPath;\n\n    public RoutingBasedOnHeadersTests()\n    {\n    }\n\n    [Fact]\n    public void Should_match_one_header_value()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, headerValue))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_one_header_value_when_more_headers()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(\"other\", \"otherValue\"))\n            .And(x => GivenIAddAHeader(headerName, headerValue))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_two_header_values_when_more_headers()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName1 = \"country_code\";\n        var headerValue1 = \"PL\";\n        var headerName2 = \"region\";\n        var headerValue2 = \"MAZ\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName1] = headerValue1,\n            [headerName2] = headerValue2,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName1, headerValue1))\n            .And(x => GivenIAddAHeader(\"other\", \"otherValue\"))\n            .And(x => GivenIAddAHeader(headerName2, headerValue2))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_match_one_header_value()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var anotherHeaderValue = \"UK\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, anotherHeaderValue))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_match_one_header_value_when_no_headers()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_match_two_header_values_when_one_different()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName1 = \"country_code\";\n        var headerValue1 = \"PL\";\n        var headerName2 = \"region\";\n        var headerValue2 = \"MAZ\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName1] = headerValue1,\n            [headerName2] = headerValue2,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName1, headerValue1))\n            .And(x => GivenIAddAHeader(\"other\", \"otherValue\"))\n            .And(x => GivenIAddAHeader(headerName2, \"anothervalue\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_match_two_header_values_when_one_not_existing()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName1 = \"country_code\";\n        var headerValue1 = \"PL\";\n        var headerName2 = \"region\";\n        var headerValue2 = \"MAZ\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName1] = headerValue1,\n            [headerName2] = headerValue2,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName1, headerValue1))\n            .And(x => GivenIAddAHeader(\"other\", \"otherValue\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_not_match_one_header_value_when_header_duplicated()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, headerValue))\n            .And(x => GivenIAddAHeader(headerName, \"othervalue\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_aggregated_route_match_header_value()\n    {\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var routeA = GivenRouteWithKey(port1, \"/a\", \"Laura\");\n        var routeB = GivenRouteWithKey(port2, \"/b\", \"Tom\");\n        var route = GivenAggRouteWithUpstreamHeaderTemplates(new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(routeA, routeB);\n        configuration.Aggregates.Add(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port1, \"/a\", HttpStatusCode.OK, Hello(\"Laura\")))\n            .And(x => GivenThereIsAServiceRunningOn(port2, \"/b\", HttpStatusCode.OK, Hello(\"Tom\")))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, headerValue))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_aggregated_route_not_match_header_value()\n    {\n        var port1 = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue = \"PL\";\n        var routeA = GivenRouteWithKey(port1, \"/a\", \"Laura\");\n        var routeB = GivenRouteWithKey(port2, \"/b\", \"Tom\");\n        var route = GivenAggRouteWithUpstreamHeaderTemplates(new()\n        {\n            [headerName] = headerValue,\n        });\n        var configuration = GivenConfiguration(routeA, routeB);\n        configuration.Aggregates.Add(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port1, \"/a\", HttpStatusCode.OK, Hello(\"Laura\")))\n            .And(x => x.GivenThereIsAServiceRunningOn(port2, \"/b\", HttpStatusCode.OK, Hello(\"Tom\")))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_header_placeholder()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"Region\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, \"/products\", \"/api.internal-{code}/products\",\n            new()\n            {\n                [headerName] = \"{header:code}\",\n            });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api.internal-uk/products\", HttpStatusCode.OK, Hello(\"UK\")))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, \"uk\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello(\"UK\")))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_header_placeholder_not_in_downstream_path()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"ProductName\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, \"/products\", \"/products-info\",\n            new()\n            {\n                [headerName] = \"product-{header:everything}\",\n            });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/products-info\", HttpStatusCode.OK, Hello(\"products\")))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, \"product-Camera\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello(\"products\")))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_distinguish_route_for_different_roles()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"Origin\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, \"/products\", \"/products-admin\",\n            new()\n            {\n                [headerName] = \"admin.xxx.com\",\n            });\n        var route2 = GivenRouteWithUpstreamHeaderTemplates(port, \"/products\", \"/products\", null);\n        var configuration = GivenConfiguration(route, route2);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/products-admin\", HttpStatusCode.OK, Hello(\"products admin\")))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, \"admin.xxx.com\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello(\"products admin\")))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_header_and_url_placeholders()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, \"/{aa}\", \"/{country_code}/{version}/{aa}\",\n            new()\n            {\n                [headerName] = \"start_{header:country_code}_version_{header:version}_end\",\n            });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/pl/v1/bb\", HttpStatusCode.OK, Hello()))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, \"start_pl_version_v1_end\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/bb\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_header_with_braces()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var route = GivenRouteWithUpstreamHeaderTemplates(port, \"/\", \"/aa\",\n            new()\n            {\n                [headerName] = \"my_{header}\",\n            });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/aa\", HttpStatusCode.OK, Hello()))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, \"my_{header}\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_match_two_headers_with_the_same_name()\n    {\n        var port = PortFinder.GetRandomPort();\n        var headerName = \"country_code\";\n        var headerValue1 = \"PL\";\n        var headerValue2 = \"UK\";\n        var multipleValues = new StringValues([headerValue1, \"{header:whatever}\"]);\n        var route = GivenRouteWithUpstreamHeaderTemplates(port,\n            new()\n            {\n                [headerName] = multipleValues.ToString(), // headerValue1 + \";{header:whatever}\",\n            });\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(headerName, headerValue1))\n            .And(x => GivenIAddAHeader(headerName, headerValue2))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(Hello()))\n            .BDDfy();\n    }\n\n    private static string Hello() => Hello(\"Jolanta\");\n    private static string Hello(string who) => $\"Hello from {who}\";\n\n    private void GivenThereIsAServiceRunningOn(int port)\n        => GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, Hello());\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\n    {\n        basePath ??= \"/\";\n        responseBody ??= Hello();\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\n        {\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\n            if (_downstreamPath != basePath)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.NotFound;\n                await context.Response.WriteAsync($\"{nameof(_downstreamPath)} is not equal to {nameof(basePath)}\");\n            }\n            else\n            {\n                context.Response.StatusCode = (int)statusCode;\n                await context.Response.WriteAsync(responseBody);\n            }\n        });\n    }\n\n    private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected);\n\n    private FileRoute GivenRouteWithKey(int port, string path = null, string key = null)\n        => GivenRoute(port, path, path).WithKey(key);\n\n    private FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, Dictionary<string, string> templates)\n    {\n        var route = GivenDefaultRoute(port);\n        route.UpstreamHeaderTemplates = templates;\n        return route;\n    }\n\n    private FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, string upstream, string downstream, Dictionary<string, string> templates)\n    {\n        var route = GivenRoute(port, upstream, downstream);\n        route.UpstreamHeaderTemplates = templates;\n        return route;\n    }\n\n    private static FileAggregateRoute GivenAggRouteWithUpstreamHeaderTemplates(Dictionary<string, string> templates) => new()\n    {\n        UpstreamPathTemplate = \"/\",\n        UpstreamHost = \"localhost\",\n        RouteKeys = [\"Laura\", \"Tom\"],\n        UpstreamHeaderTemplates = templates,\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\r\nusing Ocelot.LoadBalancer.Balancers;\r\nusing System.Web;\r\n\r\nnamespace Ocelot.AcceptanceTests.Routing;\r\n\r\npublic sealed class RoutingTests : Steps\r\n{\r\n    private string _downstreamPath;\r\n    private string _downstreamQuery;\r\n\r\n    public RoutingTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_match_forward_slash_in_pattern_before_next_forward_slash()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/api/v{apiVersion}/cards\", \"/api/v{apiVersion}/cards\")\r\n            .WithPriority(1);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/v1/aaaaaaaaa/cards\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/api/v1/aaaaaaaaa/cards\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_404_when_no_configuration_at_all()\r\n    {\r\n        this.Given(x => GivenThereIsAConfiguration(new()))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_forward_slash_and_placeholder_only()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenCatchAllRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_favouring_forward_slash_with_path_route()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenCatchAllRoute(port1);\r\n        var route2 = GivenDefaultRoute(port2);\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port1, \"/test\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/test\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_favouring_forward_slash()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenCatchAllRoute(port1);\r\n        var route2 = GivenDefaultRoute(port2);\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port2, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_favouring_forward_slash_route_because_it_is_first()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenDefaultRoute(port1);\r\n        var route2 = GivenCatchAllRoute(port2);\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port1, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_nothing_and_placeholder_only()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenCatchAllRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(string.Empty))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_simple_url()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"134\")]\r\n    public void Should_fix_issue_134()\r\n    {\r\n        var port = PortFinder.GetRandomPort(); //var port2 = PortFinder.GetRandomPort();\r\n        var methods = new string[] { HttpMethods.Options, HttpMethods.Put, HttpMethods.Get, HttpMethods.Post, HttpMethods.Delete };\r\n        var route1 = GivenRoute(port, \"/vacancy/\", \"/api/v1/vacancy\")\r\n            .WithMethods(methods);\r\n        var route2 = GivenRoute(port, \"/vacancy/{vacancyId}\", \"/api/v1/vacancy/{vacancyId}\")\r\n            .WithMethods(methods);\r\n        route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection));\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/v1/vacancy/1\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/vacancy/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_path_missing_forward_slash_as_first_char()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/\", \"/api/products\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_when_host_has_trailing_slash()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/\", \"/api/products\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(\"/products\")]\r\n    [InlineData(\"/products/\")]\r\n    public void Should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not(string url)\r\n    {\r\n        const string downstreamBasePath = \"/products\";\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, $\"{downstreamBasePath}/\", downstreamBasePath);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamBasePath, HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(url))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"649\")]\r\n    [InlineData(\"/account/authenticate\")]\r\n    [InlineData(\"/account/authenticate/\")]\r\n    public void Should_fix_issue_649(string url)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/account/authenticate/\", \"/authenticate\");\r\n        var configuration = GivenConfiguration(route);\r\n        configuration.GlobalConfiguration.BaseUrl = DownstreamUrl(port);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/authenticate\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(url))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/products\", \"/products\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/products\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"683\")]\r\n    [InlineData(\"/products/{productId}\", \"/products/{productId}\", \"/products/\")]\r\n    public void Should_return_response_200_with_empty_placeholder(string downstream, string upstream, string requestURL)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, requestURL, HttpStatusCode.OK, \"Hello from Aly\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestURL))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Aly\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_complex_url()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/products/{productId}\", \"/api/products/{productId}\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/products/1\", HttpStatusCode.OK, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Some Product\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_200_with_complex_url_that_starts_with_placeholder()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/{variantId}/products/{productId}\", \"/api/{variantId}/products/{productId}\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/23/products/1\", HttpStatusCode.OK, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"23/products/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Some Product\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_add_trailing_slash_to_downstream_url()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/products/{productId}\", \"/api/products/{productId}\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, \"/api/products/1\", HttpStatusCode.OK, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/products/1\"))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(\"/api/products/1\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_201_with_simple_url()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Post);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.Created, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_response_201_with_complex_query_string()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/newThing\", \"/newThing\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/newThing\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Feat\", \"89\")]\r\n    [InlineData(\"/api/{finalUrlPath}\", \"/api/api1/{finalUrlPath}\", \"/api/api1/product/products/categories/\", \"/api/product/products/categories/\")]\r\n    [InlineData(\"/api/{urlPath}\", \"/myApp1Name/api/{urlPath}\", \"/myApp1Name/api/products/1\", \"/api/products/1\")]\r\n    public void Should_return_response_200_with_placeholder_for_final_url_path2(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL, string downstreamPath)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstreamPathTemplate, downstreamPathTemplate);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Some Product\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestURL))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Some Product\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"748\")] // https://github.com/ThreeMammals/Ocelot/issues/748\r\n    [InlineData(\"/downstream/test/{everything}\", \"/upstream/test/{everything}\", \"/upstream/test/1\", \"/downstream/test/1\", \"?p1=v1&p2=v2&something-else=\")]\r\n    [InlineData(\"/downstream/test/{everything}\", \"/upstream/test/{everything}\", \"/upstream/test/\", \"/downstream/test/\", \"?p1=v1&p2=v2&something-else=\")]\r\n    [InlineData(\"/downstream/test/{everything}\", \"/upstream/test/{everything}\", \"/upstream/test\", \"/downstream/test\", \"?p1=v1&p2=v2&something-else=\")]\r\n    [InlineData(\"/downstream/test/{everything}\", \"/upstream/test/{everything}\", \"/upstream/test123\", null, null)]\r\n    [InlineData(\"/downstream/{version}/test/{everything}\", \"/upstream/{version}/test/{everything}\", \"/upstream/v1/test/123\", \"/downstream/v1/test/123\", \"?p1=v1&p2=v2&something-else=\")]\r\n    [InlineData(\"/downstream/{version}/test\", \"/upstream/{version}/test\", \"/upstream/v1/test\", \"/downstream/v1/test\", \"?p1=v1&p2=v2&something-else=\")]\r\n    [InlineData(\"/downstream/{version}/test\", \"/upstream/{version}/test\", \"/upstream/test\", null, null)]\r\n    public void Should_return_correct_downstream_when_omitting_ending_placeholder(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL, string downstreamURL, string queryString)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstreamPathTemplate, downstreamPathTemplate);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Aly\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestURL))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamURL))\r\n\r\n            // Now check the same URL but with query string\r\n            // Catch-All placeholder should forward any path + query string combinations to the downstream service\r\n            // More: https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders:~:text=This%20will%20forward%20any%20path%20%2B%20query%20string%20combinations%20to%20the%20downstream%20service%20after%20the%20path%20%2Fapi.\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestURL + queryString))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamURL))\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Trait(\"PR\", \"1911\")]\r\n    [Trait(\"Link\", \"https://ocelot.readthedocs.io/en/latest/features/routing.html#catch-all-query-string\")]\r\n    [Theory(DisplayName = \"Catch All Query String should be forwarded with all query string parameters with(out) last slash\")]\r\n    [InlineData(\"/apipath/contracts?{everything}\", \"/contracts?{everything}\", \"/contracts\", \"/apipath/contracts\", \"\")]\r\n    [InlineData(\"/apipath/contracts?{everything}\", \"/contracts?{everything}\", \"/contracts?\", \"/apipath/contracts\", \"\")]\r\n    [InlineData(\"/apipath/contracts?{everything}\", \"/contracts?{everything}\", \"/contracts?p1=v1&p2=v2\", \"/apipath/contracts\", \"?p1=v1&p2=v2\")]\r\n    [InlineData(\"/apipath/contracts/?{everything}\", \"/contracts/?{everything}\", \"/contracts/?\", \"/apipath/contracts/\", \"\")]\r\n    [InlineData(\"/apipath/contracts/?{everything}\", \"/contracts/?{everything}\", \"/contracts/?p3=v3&p4=v4\", \"/apipath/contracts/\", \"?p3=v3&p4=v4\")]\r\n    [InlineData(\"/apipath/contracts?{everything}\", \"/contracts?{everything}\", \"/contracts?filter=(-something+123+else)\", \"/apipath/contracts\", \"?filter=(-something%20123%20else)\")]\r\n    public void Should_forward_Catch_All_query_string_when_last_slash(string downstream, string upstream, string requestURL, string downstreamPath, string queryString)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Hello from Raman\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestURL))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath)) // !\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString)) // !!\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Raman\"))\r\n            .BDDfy();\r\n    }\r\n    \r\n    [Theory]\r\n    [Trait(\"Bug\", \"2199\")]\r\n    [Trait(\"Feat\", \"2200\")]\r\n    [InlineData(\"/api/invoices/{url0}-{url1}-{url2}\", \"/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}\", \r\n        \"/api/invoices_abc/def-ghi_abcd/xyz?urlId=bla\", \"/api/invoices/abc-def-ghi\", \"?urlId=bla\")]\r\n    [InlineData(\"/api/products/{category}-{subcategory}/{filter}\", \"/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}\", \r\n        \"/api/products_electronics/computers_details/123?filter=active\", \"/api/products/electronics-computers/active\", \"\")]\r\n    [InlineData(\"/api/users/{userId}/posts/{postId}/{lang}\", \"/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}\", \r\n        \"/api/users/101/2022_content/2024?lang=en\", \"/api/users/101/posts/2022/en\", \"\")]\r\n    [InlineData(\"/api/categories/{cat}-{subcat}?sort={sort}\", \"/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}\", \r\n        \"/api/categories_home/furniture_items/789?sort=asc\", \"/api/categories/home-furniture\", \"?sort=asc\")]\r\n    [InlineData(\"/api/orders/{order}-{detail}?status={status}\", \"/api/orders_{order}/{detail}_invoice/{ref}?status={status}\", \r\n        \"/api/orders_987/abc_invoice/123?status=shipped\", \"/api/orders/987-abc\", \"?status=shipped\")]\r\n    [InlineData(\"/api/transactions/{type}-{region}\", \"/api/transactions_{type}/{region}_summary/{year}?q={query}\", \r\n        \"/api/transactions_sales/NA_summary/2024?q=forecast\", \"/api/transactions/sales-NA\", \"?q=forecast\")]\r\n    [InlineData(\"/api/resources/{resource}-{subresource}\", \"/api/resources_{resource}/{subresource}_data/{id}?key={apikey}\", \r\n        \"/api/resources_images/photos_data/555?key=xyz123\", \"/api/resources/images-photos\", \"?key=xyz123\")]\r\n    [InlineData(\"/api/accounts/{account}-{detail}\", \"/api/accounts_{account}/{detail}_info/{id}?opt={option}\", \r\n        \"/api/accounts_admin/settings_info/101?opt=true\", \"/api/accounts/admin-settings\", \"?opt=true\")]\r\n    public void ShouldMatchComplexQueriesWithEmbeddedPlaceholders(string downstream, string upstream, string requestUrl, string downstreamPath, string queryString)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Hello from Guillaume\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestUrl))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath))\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString))\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Guillaume\"))\r\n            .BDDfy();\r\n    }\r\n    \r\n    [Theory]\r\n    [Trait(\"Bug\", \"2209\")]\r\n    [InlineData(\"/api/invoices/{url0}-{url1}-{url2}\", \"/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}\", \r\n        \"/api/InvoIces_abc/def-ghi_abcd/xyz?urlId=bla\", \"/api/invoices/abc-def-ghi\", \"?urlId=bla\")]\r\n    [InlineData(\"/api/products/{category}-{subcategory}/{filter}\", \"/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}\", \r\n        \"/API/PRODUCTS_electronics/computers_details/123?filter=active\", \"/api/products/electronics-computers/active\", \"\")]\r\n    [InlineData(\"/api/users/{userId}/posts/{postId}/{lang}\", \"/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}\", \r\n        \"/api/UsErS/101/2022_content/2024?lang=en\", \"/api/users/101/posts/2022/en\", \"\")]\r\n    [InlineData(\"/api/categories/{cat}-{subcat}?sort={sort}\", \"/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}\", \r\n        \"/api/CATEGORIES_home/furniture_items/789?sort=asc\", \"/api/categories/home-furniture\", \"?sort=asc\")]\r\n    [InlineData(\"/api/orders/{order}-{detail}?status={status}\", \"/api/orders_{order}/{detail}_invoice/{ref}?status={status}\", \r\n        \"/API/ORDERS_987/abc_invOiCE/123?status=shipped\", \"/api/orders/987-abc\", \"?status=shipped\")]\r\n    [InlineData(\"/api/transactions/{type}-{region}\", \"/api/transactions_{type}/{region}_summary/{year}?q={query}\", \r\n        \"/api/TRanSacTIONS_sales/NA_summary/2024?q=forecast\", \"/api/transactions/sales-NA\", \"?q=forecast\")]\r\n    public void ShouldMatchComplexQueriesCaseInsensitive(string downstream, string upstream, string requestUrl, string downstreamPath, string queryString)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Hello from Guillaume\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestUrl))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath))\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString))\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Guillaume\"))\r\n            .BDDfy();\r\n    }\r\n    \r\n    [Theory]\r\n    [Trait(\"Bug\", \"2209\")]\r\n    [InlineData(\"/api/invoices/{url0}-{url1}-{url2}\", \"/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}\", \r\n        \"/api/InvoIces_abc/def-ghi_abcd/xyz?urlId=bla\", \"/api/invoices/abc-def-ghi\")]\r\n    [InlineData(\"/api/products/{category}-{subcategory}/{filter}\", \"/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}\", \r\n        \"/API/PRODUCTS_electronics/computers_details/123?filter=active\", \"/api/products/electronics-computers/active\")]\r\n    [InlineData(\"/api/users/{userId}/posts/{postId}/{lang}\", \"/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}\", \r\n        \"/api/UsErS/101/2022_content/2024?lang=en\", \"/api/users/101/posts/2022/en\")]\r\n    [InlineData(\"/api/categories/{cat}-{subcat}?sort={sort}\", \"/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}\", \r\n        \"/api/CATEGORIES_home/furniture_items/789?sort=asc\", \"/api/categories/home-furniture\")]\r\n    [InlineData(\"/api/orders/{order}-{detail}?status={status}\", \"/api/orders_{order}/{detail}_invoice/{ref}?status={status}\", \r\n        \"/API/ORDERS_987/abc_invOiCE/123?status=shipped\", \"/api/orders/987-abc\")]\r\n    [InlineData(\"/api/transactions/{type}-{region}\", \"/api/transactions_{type}/{region}_summary/{year}?q={query}\", \r\n        \"/api/TRanSacTIONS_sales/NA_summary/2024?q=forecast\", \"/api/transactions/sales-NA\")]\r\n    public void ShouldNotMatchComplexQueriesCaseSensitive(string downstream, string upstream, string requestUrl, string downstreamPath)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        route.RouteIsCaseSensitive = true;\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Hello from Guillaume\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestUrl))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"2212\")]\r\n    [InlineData(\"/data-registers/{version}/it/{everything}\", \"/dati-registri/{version}/{everything}\", \"/dati-registri/v1.0/operatore/R80QQ5J9600/valida\", \"/data-registers/v1.0/it/operatore/R80QQ5J9600/valida\")]\r\n    [InlineData(\"/files/{version}/uploads/{everything}\", \"/data/{version}/storage/{everything}\", \"/data/v2.0/storage/images/photos/nature\", \"/files/v2.0/uploads/images/photos/nature\")]\r\n    [InlineData(\"/resources/{area}/details/{everything}\", \"/api/resources/{area}/info/{everything}\", \"/api/resources/global/info/stats/2024/data\", \"/resources/global/details/stats/2024/data\")]\r\n    [InlineData(\"/users/{userId}/logs/{everything}\", \"/data/users/{userId}/activity/{everything}\", \"/data/users/12345/activity/session/login/2024\", \"/users/12345/logs/session/login/2024\")]\r\n    [InlineData(\"/orders/{orderId}/items/{everything}\", \"/ecommerce/{orderId}/details/{everything}\", \"/ecommerce/98765/details/category/electronics/phone\", \"/orders/98765/items/category/electronics/phone\")]\r\n    [InlineData(\"/tasks/{taskId}/subtasks/{everything}\", \"/work/{taskId}/breakdown/{everything}\", \"/work/56789/breakdown/phase/3/step/2\", \"/tasks/56789/subtasks/phase/3/step/2\")]\r\n    [InlineData(\"/configs/{env}/overrides/{everything}\", \"/settings/{env}/{everything}\", \"/settings/prod/feature/toggles\", \"/configs/prod/overrides/feature/toggles\")]\r\n    public void OnlyTheLastPlaceholderShouldMatchSeveralSegments(string downstream, string upstream, string requestUrl, string downstreamPath)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, upstream, downstream);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, \"Hello from Guillaume\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(requestUrl))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath))\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Guillaume\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"91, 94\")]\r\n    public void Should_return_response_201_with_simple_url_and_multiple_upstream_http_method()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Get, HttpMethods.Post);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.Created, nameof(HttpStatusCode.Created)))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", \"postContent\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created))\r\n            .And(x => ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.Created)))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"91, 94\")]\r\n    public void Should_return_response_200_with_simple_url_and_any_upstream_http_method()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port).WithMethods();\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"134\")]\r\n    public void Should_return_404_when_calling_upstream_route_with_no_matching_downstream_route()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var methods = new string[] { HttpMethods.Options, HttpMethods.Put, HttpMethods.Get, HttpMethods.Post, HttpMethods.Delete };\r\n        var route1 = GivenRoute(port, \"/vacancy/\", \"/api/v1/vacancy\").WithMethods(methods);\r\n        var route2 = GivenRoute(port, \"/vacancy/{vacancyId}\", \"/api/v1/vacancy/{vacancyId}\").WithMethods(methods);\r\n        route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection));\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/v1/vacancy/1\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"api/vacancy/1\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"145\")]\r\n    public void Should_not_set_trailing_slash_on_url_template()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/platform/{url}\", \"/api/{url}\");\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/swagger/lib/backbone-min.js\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/platform/swagger/lib/backbone-min.js\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/api/swagger/lib/backbone-min.js\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"270, 272\")]\r\n    public void Should_use_priority()\r\n    {\r\n        var port1 = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenRoute(port1, \"/goods/{url}\", \"/goods/{url}\")\r\n            .WithPriority(0);\r\n        var route2 = GivenRoute(port2, \"/goods/delete\", \"/goods/delete\");\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port2, \"/goods/delete\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/goods/delete\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"548\")]\r\n    public void Should_match_multiple_paths_with_catch_all()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenCatchAllRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/test/toot\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/test/toot\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"271\")]\r\n    public void Should_fix_issue_271()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var port2 = PortFinder.GetRandomPort();\r\n        var route1 = GivenRoute(port, \"/api/v1/{everything}\", \"/api/v1/{everything}\")\r\n            .WithMethods(HttpMethods.Get, HttpMethods.Put, HttpMethods.Post);\r\n        var route2 = GivenRoute(port2, \"/connect/token\", \"/connect/token\")\r\n            .WithMethods(HttpMethods.Post);\r\n        var configuration = GivenConfiguration(route1, route2);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/api/v1/modules/Test\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/api/v1/modules/Test\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"2116\")]\r\n    [InlineData(\"debug()\")] // no query\r\n    [InlineData(\"debug%28%29\")] // debug()\r\n    public void Should_change_downstream_path_by_upstream_path_when_path_contains_malicious_characters(string path)\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenRoute(port, \"/api/{path}\", \"/routed/api/{path}\");\r\n        var configuration = GivenConfiguration(route);\r\n        var decodedDownstreamUrlPath = $\"/routed/api/{HttpUtility.UrlDecode(path)}\";\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, decodedDownstreamUrlPath, HttpStatusCode.OK, string.Empty))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/{path}\")) // should be encoded\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheDownstreamUrlPathShouldBe(decodedDownstreamUrlPath))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"2064\")]\r\n    [Trait(\"Discus\", \"2065\")]\r\n    public void Should_match_correct_route_when_placeholder_appears_after_query_start()\r\n    {\r\n        const string DownstreamPath = \"/1/products/1\";\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(\r\n            GivenRoute(port, \"/{tenantId}/products?{everything}\", \"/{tenantId}/products?{everything}\"), // This route should NOT BE matched\r\n            GivenRoute(port, \"/{tenantId}/products/{everything}\", \"/{tenantId}/products/{everything}\")); // This route should BE matched\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, DownstreamPath, HttpStatusCode.OK, \"Hello from Finn\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/1/products/1\"))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(DownstreamPath))\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty))\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Finn\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"2132\")]\r\n    public void Should_match_correct_route_when_a_configuration_exists_with_query_param_wildcard()\r\n    {\r\n        const string DownstreamPath = \"/api/v1/apple\";\r\n        var port = PortFinder.GetRandomPort();\r\n        var configuration = GivenConfiguration(\r\n            GivenRoute(port, \"/api/v1/abc?{everything}\",  \"/api/v1/abc?{everything}\"), // This route should NOT be matched\r\n            GivenRoute(port, \"/api/v1/abc2/{everything}\", \"/api/v1/{everything}\")); // This route should be matched\r\n        this.Given(x => GivenThereIsAServiceRunningOn(port, DownstreamPath, HttpStatusCode.OK, \"Hello from Finn\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/api/v1/abc2/apple?isRequired=1\"))\r\n            .Then(x => ThenTheDownstreamUrlPathShouldBe(DownstreamPath))\r\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(\"?isRequired=1\"))\r\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Finn\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatusCode);\r\n\r\n        Task MapStatusCode(HttpContext context)\r\n        {\r\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value)\r\n                ? context.Request.PathBase.Value + context.Request.Path.Value\r\n                : context.Request.Path.Value;\r\n            _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;\r\n            bool oK = _downstreamPath == basePath;\r\n            context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound;\r\n            return context.Response.WriteAsync(oK ? responseBody : \"Downstream path didn't match base path\");\r\n        }\r\n    }\r\n\r\n    private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath)\r\n    {\r\n        _downstreamPath.ShouldBe(expectedDownstreamPath);\r\n    }\r\n\r\n    private void ThenTheDownstreamUrlQueryStringShouldBe(string expectedQueryString)\r\n    {\r\n        _downstreamQuery.ShouldBe(expectedQueryString);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.AcceptanceTests.Routing;\n\npublic sealed class RoutingWithQueryStringTests : Steps\n{\n    public RoutingWithQueryStringTests()\n    {\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_query_string_template()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/units/{subscriptionId}/{unitId}/updates\",\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/subscriptions/{subscriptionId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/units/{subscriptionId}/{unitId}/updates\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/api/subscriptions/{subscriptionId}/updates\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?unitId={unitId}\"))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"952\")]\n    [InlineData(\"\")]\n    [InlineData(\"&x=xxx\")]\n    public void Should_return_200_with_query_string_template_different_keys(string additionalParams)\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/units/{subscriptionId}/updates?unit={unit}\",\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unit}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/subscriptions/{subscriptionId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/api/subscriptions/{subscriptionId}/updates\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?unitId={unitId}{additionalParams}\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"952\")]\n    public void Should_map_query_parameters_with_different_names()\n    {\n        const string userId = \"webley\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/users?userId={userId}\",\n            \"/persons?personId={userId}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/persons\", \"Hello from @webley\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/users?userId={userId}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from @webley\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/persons\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?personId={userId}\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"952\")]\n    public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ()\n    {\n        const string uid = \"webley\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/users?userId={uid}\",\n            \"/persons?personId={uid}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/persons\", \"Hello from @webley\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/users?userId={uid}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from @webley\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/persons\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?personId={uid}&userId={uid}\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"952\")]\n    public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ_case_sensitive()\n    {\n        const string userid = \"webley\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/users?userId={userid}\",\n            \"/persons?personId={userid}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/persons\", \"Hello from @webley\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/users?userId={userid}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from @webley\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/persons\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?personId={userid}&userId={userid}\"))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1174\")] // https://github.com/ThreeMammals/Ocelot/issues/1174\n    [InlineData(\"projectNumber=45&startDate=2019-12-12&endDate=2019-12-12\", \"?projectNumber=45&startDate=2019-12-12&endDate=2019-12-12\")]\n    [InlineData(\"$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z\", \"?$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00%3A00%3A00z%20and%20DateOfSale%20le%202020-03-15T00%3A00%3A00z\")]\n    public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expected)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/contracts?{everythingelse}\",\n            \"/api/contracts?{everythingelse}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/contracts\", \"Hello from @sunilk3\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/contracts?{everythingelse}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from @sunilk3\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/api/contracts\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(expected))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"548\")] // https://github.com/ThreeMammals/Ocelot/issues/548\n    [Trait(\"Commit\", \"00a6000\")] // https://github.com/ThreeMammals/Ocelot/commit/00a600064deea0877058d04e6189d7e0278c99a5\n    [Trait(\"Release\", \"10.0.4\")]\n    public void Should_return_response_200_with_odata_query_string()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenCatchAllRoute(port);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/odata/customers\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/odata/customers?$filter=Name eq 'Sam' \"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/odata/customers\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(\"?$filter=Name%20eq%20%27Sam%27\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public void Should_return_response_200_with_query_string_upstream_template()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\",\n            \"/api/units/{subscriptionId}/{unitId}/updates\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/units/{subscriptionId}/{unitId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/api/units/{subscriptionId}/{unitId}/updates\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public void Should_return_response_404_with_query_string_upstream_template_no_query_string()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\",\n            \"/api/units/{subscriptionId}/{unitId}/updates\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/units/{subscriptionId}/{unitId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/subscriptions/{subscriptionId}/updates\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public void Should_return_response_404_with_query_string_upstream_template_different_query_string()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\",\n            \"/api/units/{subscriptionId}/{unitId}/updates\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/units/{subscriptionId}/{unitId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/subscriptions/{subscriptionId}/updates?test=1\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public void Should_return_response_200_with_query_string_upstream_template_multiple_params()\n    {\n        var subscriptionId = Guid.NewGuid().ToString();\n        var unitId = Guid.NewGuid().ToString();\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\",\n            \"/api/units/{subscriptionId}/{unitId}/updates\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/api/units/{subscriptionId}/{unitId}/updates\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/api/units/{subscriptionId}/{unitId}/updates\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(\"?productId=1\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2002\")] // https://github.com/ThreeMammals/Ocelot/issues/2002\n    [Trait(\"Commit\", \"a034e8c\")] // https://github.com/ThreeMammals/Ocelot/commit/a034e8c1e3fc23a086ad10000c85615b9696a43e\n    [Trait(\"Release\", \"23.3.0\")]\n    public void Should_map_when_query_parameters_has_same_names_with_placeholder()\n    {\n        const string username = \"bbenameur\";\n        const string groupName = \"Paris\";\n        const string roleid = \"123456\";\n        const string everything = \"something=9874565\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            \"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}\",\n            \"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/account/{username}/groups/{groupName}/roles\", \"Hello from Béchir\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Béchir\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/account/{username}/groups/{groupName}/roles\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?roleId={roleid}&{everything}\"))\n            .BDDfy();\n    }\n\n    /// <summary>\n    /// To reproduce 1288: query string should contain the placeholder name and value.\n    /// </summary>\n    [Fact]\n    [Trait(\"Bug\", \"1288\")]\n    public void Should_copy_query_string_to_downstream_path()\n    {\n        var idName = \"id\";\n        var idValue = \"3\";\n        var queryName = idName + \"1\";\n        var queryValue = \"2\" + idValue + \"12\";\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port,\n            $\"/safe/{{{idName}}}\",\n            $\"/cpx/t1/{{{idName}}}\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, $\"/cpx/t1/{idValue}\", \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway($\"/safe/{idValue}?{queryName}={queryValue}\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe($\"/cpx/t1/{idValue}\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe($\"?{queryName}={queryValue}\"))\n            .BDDfy();\n    }\n\n    #region PR 2351\n    [Fact]\n    [Trait(\"PR\", \"2351\")] // https://github.com/ThreeMammals/Ocelot/pull/2351\n    [Trait(\"Bug\", \"2346\")] // https://github.com/ThreeMammals/Ocelot/issues/2346\n    public void Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder_as_a_Catch_All_Query_String()\n    {\n        // This test ensures that a placeholder like {id} does not remove or corrupt parameters like customer_id\n        const string customer_id = \"12345\";\n        var port = PortFinder.GetRandomPort();\n        var route1 = GivenRoute(port,\n            upstream: \"/finance/v1/payment-methods?{id}\", // Catch All query string placeholder -> https://ocelot.readthedocs.io/en/latest/features/routing.html#catch-all-query-string\n            downstream: \"/v1/payment-methods?{id}\");\n        var route2 = GivenRoute(port,\n            upstream: \"/finance/v1/payment-methods\",\n            downstream: \"/v1/payment-methods\");\n        var configuration = GivenConfiguration(route1, route2);\n        var query = $\"?{nameof(customer_id)}={customer_id}\";\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/v1/payment-methods\", \"Hello from Bhargav\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate.Replace(\"?{id}\", query)))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Bhargav\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/v1/payment-methods\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(query))\n            .When(x => WhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheDownstreamUrlPathShouldBe(\"/v1/payment-methods\"))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2351\")] // https://github.com/ThreeMammals/Ocelot/pull/2351\n    [Trait(\"Bug\", \"2346\")] // https://github.com/ThreeMammals/Ocelot/issues/2346\n    [InlineData(\"/finance/v1/payment-methods/{id}\", \"/v1/payment-methods/{id}\", // Placeholder: {id}, Query: customer_id\n        \"/finance/v1/payment-methods/?customer_id=123\",\n        \"/v1/payment-methods/\", \"?customer_id=123\")]\n    [InlineData(\"/finance/v1/payment-methods/{id}/orders/{oid}\", \"/v1/orders/{id}/{oid}\", // Placeholder: {id}, Query: orderid, customer_id\n        \"/finance/v1/payment-methods/1/orders/2?id=1&oid=2&orderid=3&customer_id=4\",\n        \"/v1/orders/1/2\", \"?orderid=3&customer_id=4\")]\n    [InlineData(\"/finance/v1/users/{customer_id}\", \"/v1/users/{customer_id}\", // Placeholder: {customer_id}, Query: id\n        \"/finance/v1/users/123?id=999&customer_id=x\",\n        \"/v1/users/123\", \"?id=999\")]\n    [InlineData(\"/finance/v1/items/{id}/{Id}\", \"/v1/items/{Id}/{id}\", // Placeholder: {Id}, Query: id, Id\n        \"/finance/v1/items/1/2?id=3&Id=4&ID=5&extra=x\", // case insensitive params will be removed\n        \"/v1/items/2/1\", \"?extra=x\")]\n    [InlineData(\"/finance/v1/data/{id}\", \"/v1/data/{id}\", // Placeholder: {id}, Query: xid, idx, id\n        \"/finance/v1/data/123/?xid=1&idx=2&id=3&extra=x\",\n        \"/v1/data/123/\", \"?xid=1&idx=2&extra=x\")]\n    [InlineData(\"/finance/v1/records/{id1}\", \"/v1/records/{id1}\", // Placeholder: {id1}, Query: id1, id10\n        \"/finance/v1/records/123/?id1=1&id10=10&extra=x\",\n        \"/v1/records/123/\", \"?id10=10&extra=x\")]\n    [InlineData(\"/finance/v1/alpha/{id}\", \"/v1/alpha/{id}\", // Placeholder: {id}, Query: id_, _id, id\n        \"/finance/v1/alpha/123/?id_=1&_id=2&id=3&extra=x\",\n        \"/v1/alpha/123/\", \"?id_=1&_id=2&extra=x\")]\n    [InlineData(\"/finance/v2/orders/{id}\", \"/v2/orders/{id}\", // Placeholder: {id}, Query: multiple query parameters\n        \"/finance/v2/orders/42?status=active&customer_id=123&extra=x\",\n        \"/v2/orders/42\", \"?status=active&customer_id=123&extra=x\")]\n    public void Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder(\n        string upstream, string downstream, string url, string path, string query)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, upstream, downstream);\n        var configuration = GivenConfiguration(route);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, path, \"Hello from Bhargav\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(url))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Bhargav\"))\n            .And(x => ThenTheDownstreamUrlPathShouldBe(path))\n            .And(x => ThenTheDownstreamUrlQueryStringShouldBe(query))\n            .BDDfy();\n    }\n    #endregion of PR 2351\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, string responseBody)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatusCode);\n\n        Task MapStatusCode(HttpContext context)\n        {\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value)\n                ? context.Request.PathBase.Value + context.Request.Path.Value\n                : context.Request.Path.Value;\n            _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;\n            bool oK = _downstreamPath == basePath; // || _downstreamQuery != queryString; // TODO this is strict assertion, it can be without query string\n            context.Response.StatusCode = oK ? StatusCodes.Status200OK : StatusCodes.Status404NotFound;\n            return context.Response.WriteAsync(oK ? responseBody : \"Downstream path didn't match base path\");\n        }\n    }\n\n    private string _downstreamPath;\n    private string _downstreamQuery;\n    private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected);\n    private void ThenTheDownstreamUrlQueryStringShouldBe(string expected) => _downstreamQuery.ShouldBe(expected);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Routing/UpstreamHostTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\n\nnamespace Ocelot.AcceptanceTests.Routing;\n\n/// <summary>\n/// Feature: <see href=\"https://ocelot.readthedocs.io/en/latest/features/routing.html#upstream-host\">Upstream Host</see>,\n/// with docs: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-host-4\">Upstream Host</see>.\n/// </summary>\npublic sealed class UpstreamHostTests : Steps\n{\n    public UpstreamHostTests()\n    {\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url_and_hosts_match()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port).WithUpstreamHost(\"localhost\");\n        var configuration = GivenConfiguration(route);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes()\n    {\n        var port = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port).WithUpstreamHost(\"localhost\");\n        var route2 = GivenDefaultRoute(port2).WithUpstreamHost(\"DONTMATCH\");\n        var configuration = GivenConfiguration(route, route2);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed()\n    {\n        var port = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port).WithUpstreamHost(\"DONTMATCH\");\n        var route2 = GivenDefaultRoute(port2).WithUpstreamHost(\"localhost\");\n        var configuration = GivenConfiguration(route, route2);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port2, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed_with_no_host_first()\n    {\n        var port = PortFinder.GetRandomPort();\n        var port2 = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port).WithUpstreamHost(null);\n        var route2 = GivenDefaultRoute(port2).WithUpstreamHost(\"localhost\");\n        var configuration = GivenConfiguration(route, route2);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port2, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_404_with_simple_url_and_hosts_dont_match()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port).WithUpstreamHost(\"127.0.0.20:5000\");\n        var configuration = GivenConfiguration(route);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))\n            .BDDfy();\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\n        {\n            var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\n            bool oK = downstreamPath == basePath;\n            context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound;\n            return context.Response.WriteAsync(oK ? responseBody : \"downstream path didn't match base path\");\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Security/SecurityOptionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.AcceptanceTests.Security;\n\npublic sealed class SecurityOptionsTests: Steps\n{\n    public SecurityOptionsTests()\n    {\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2170\")]\n    public void Should_call_with_allowed_ip_in_global_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.35\")[0];\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\");\n        var configuration = GivenGlobalConfiguration(route, \"192.168.1.30-50\", \"192.168.1.1-100\");\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2170\")]\n    public void Should_block_call_with_blocked_ip_in_global_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.55\")[0];\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\");\n        var configuration = GivenGlobalConfiguration(route, \"192.168.1.30-50\", \"192.168.1.1-100\");\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized));\n    }\n\n    [Fact]\n    public void Should_call_with_allowed_ip_in_route_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        var securityConfig = new FileSecurityOptions\n        {\n            IPAllowedList = new() { \"192.168.1.1\" },\n        };\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\", securityConfig);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK));\n    }\n\n    [Fact]\n    public void Should_block_call_with_blocked_ip_in_route_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        var securityConfig = new FileSecurityOptions\n        {\n            IPBlockedList = new() { \"192.168.1.1\" },\n        };\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\", securityConfig);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2170\")]\n    public void Should_call_with_allowed_ip_in_route_config_and_blocked_ip_in_global_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.55\")[0];\n        var securityConfig = new FileSecurityOptions\n        {\n            IPAllowedList = new() { \"192.168.1.55\" },\n        };\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\", securityConfig);\n        var configuration = GivenGlobalConfiguration(route, \"192.168.1.30-50\", \"192.168.1.1-100\");\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n           .And(x => GivenThereIsAConfiguration(configuration))\n           .And(x => GivenOcelotIsRunning())\n           .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n           .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n           .Then(x => ThenTheResponseBodyShouldBe(\"Hello from Fabrizio\"));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2170\")]\n    public void Should_block_call_with_blocked_ip_in_route_config_and_allowed_ip_in_global_config()\n    {\n        var port = PortFinder.GetRandomPort();\n        var ip = Dns.GetHostAddresses(\"192.168.1.35\")[0];\n        var securityConfig = new FileSecurityOptions\n        {\n            IPBlockedList = new() { \"192.168.1.35\" },\n        };\n        var route = GivenRoute(port, \"/myPath\", \"/worldPath\", securityConfig);\n        var configuration = GivenGlobalConfiguration(route, \"192.168.1.30-50\", \"192.168.1.1-100\");\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip))\n           .And(x => GivenThereIsAConfiguration(configuration))\n           .And(x => GivenOcelotIsRunning())\n           .When(x => WhenIGetUrlOnTheApiGateway(\"/worldPath\"))\n           .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized));\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, IPAddress ipAddess)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            context.Connection.RemoteIpAddress = ipAddess;\n            context.Response.StatusCode = (int)HttpStatusCode.OK;\n            return context.Response.WriteAsync(\"Hello from Fabrizio\");\n        });\n    }\n\n    private FileConfiguration GivenGlobalConfiguration(FileRoute route, string allowed, string blocked, bool exclude = true)\n    {\n        var config = GivenConfiguration(route);\n        config.GlobalConfiguration.SecurityOptions = new FileSecurityOptions\n        {\n            IPAllowedList = new() { allowed },\n            IPBlockedList = new() { blocked },\n            ExcludeAllowedFromBlocked = exclude,\n        };\n        return config;\n    }\n\n    private static FileRoute GivenRoute(int port, string downstream, string upstream, FileSecurityOptions fileSecurityOptions = null) => new()\n    {\n        DownstreamPathTemplate = downstream,\n        UpstreamPathTemplate = upstream,\n        UpstreamHttpMethod = [HttpMethods.Get],\n        SecurityOptions = fileSecurityOptions ?? new(),\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/SequentialTests.cs",
    "content": "﻿[assembly: CollectionBehavior(DisableTestParallelization = true)]\n\nnamespace Ocelot.AcceptanceTests;\n\n/// <summary>\n/// Apply <see cref=\"CollectionAttribute\"/> to classes to disable parallelization.\n/// </summary>\n[CollectionDefinition(nameof(SequentialTests), DisableParallelization = true)]\npublic class SequentialTests\n{\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulAgentServiceExtensions.cs",
    "content": "﻿using Consul;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\ninternal static class ConsulAgentServiceExtensions\n{\n    public static AgentService WithServiceName(this AgentService agent, string serviceName)\n    {\n        agent.Service = serviceName;\n        return agent;\n    }\n\n    public static AgentService WithPort(this AgentService agent, int port)\n    {\n        agent.Port = port;\n        return agent;\n    }\n\n    public static AgentService WithAddress(this AgentService agent, string address)\n    {\n        agent.Address = address;\n        return agent;\n    }\n\n    public static ServiceEntry ToServiceEntry(this AgentService agent) => new()\n    {\n        Service = agent,\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Newtonsoft.Json;\nusing Ocelot.AcceptanceTests.RateLimiting;\nusing Ocelot.Cache;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Consul;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class ConsulConfigurationInConsulTests : RateLimitingSteps\n{\n    private FileConfiguration _config;\n    private readonly List<ServiceEntry> _consulServices;\n\n    public ConsulConfigurationInConsulTests()\n    {\n        _consulServices = new List<ServiceEntry>();\n    }\n\n    [Fact]\n    public void Should_return_response_200_with_simple_url()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n                {\n                    new()\n                    {\n                        DownstreamPathTemplate = \"/\",\n                        DownstreamScheme = \"http\",\n                        DownstreamHostAndPorts = new List<FileHostAndPort>\n                        {\n                            new()\n                            {\n                                Host = \"localhost\",\n                                Port = servicePort,\n                            },\n                        },\n                        UpstreamPathTemplate = \"/\",\n                        UpstreamHttpMethod = [\"Get\"],\n                    },\n                },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n        this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty))\n            .And(x => x.GivenThereIsAServiceRunningOn(servicePort, string.Empty, HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_load_configuration_out_of_consul()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n\n        var configuration = new FileConfiguration\n        {\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n        var consulConfig = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/status\",\n                    DownstreamScheme = \"http\",\n                    DownstreamHostAndPorts = new List<FileHostAndPort>\n                    {\n                        new()\n                        {\n                            Host = \"localhost\",\n                            Port = servicePort,\n                        },\n                    },\n                    UpstreamPathTemplate = \"/cs/status\",\n                    UpstreamHttpMethod = [\"Get\"],\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n\n        this.Given(x => GivenTheConsulConfigurationIs(consulConfig))\n            .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty))\n            .And(x => x.GivenThereIsAServiceRunningOn(servicePort, \"/status\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/cs/status\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_load_configuration_out_of_consul_if_it_is_changed()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n\n        var configuration = new FileConfiguration\n        {\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n        var consulConfig = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/status\",\n                    DownstreamScheme = \"http\",\n                    DownstreamHostAndPorts = new List<FileHostAndPort>\n                    {\n                        new()\n                        {\n                            Host = \"localhost\",\n                            Port = servicePort,\n                        },\n                    },\n                    UpstreamPathTemplate = \"/cs/status\",\n                    UpstreamHttpMethod = [\"Get\"],\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n\n        var secondConsulConfig = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/status\",\n                    DownstreamScheme = \"http\",\n                    DownstreamHostAndPorts = new List<FileHostAndPort>\n                    {\n                        new()\n                        {\n                            Host = \"localhost\",\n                            Port = servicePort,\n                        },\n                    },\n                    UpstreamPathTemplate = \"/cs/status/awesome\",\n                    UpstreamHttpMethod = [\"Get\"],\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n\n        this.Given(x => GivenTheConsulConfigurationIs(consulConfig))\n            .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty))\n            .And(x => x.GivenThereIsAServiceRunningOn(servicePort, \"/status\", HttpStatusCode.OK, \"Hello from Laura\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig())\n            .And(x => WhenIGetUrlOnTheApiGateway(\"/cs/status\"))\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .When(x => GivenTheConsulConfigurationIs(secondConsulConfig))\n            .Then(x => ThenTheConfigIsUpdatedInOcelot())\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        const string serviceName = \"web\";\n        var servicePort = PortFinder.GetRandomPort();\n        var serviceEntryOne = new ServiceEntry\n        {\n            Service = new AgentService\n            {\n                Service = serviceName,\n                Address = \"localhost\",\n                Port = servicePort,\n                ID = \"web_90_0_2_224_8080\",\n                Tags = new[] { \"version-v1\" },\n            },\n        };\n\n        var consulConfig = new FileConfiguration\n        {\n            DynamicRoutes = new()\n            {\n                new()\n                {\n                    ServiceName = serviceName,\n                    RateLimitRule = new FileRateLimitByHeaderRule\n                    {\n                        EnableRateLimiting = true,\n                        ClientWhitelist = new List<string>(),\n                        Limit = 3,\n                        Period = \"1s\",\n                        PeriodTimespan = 1000,\n                    },\n                },\n            },\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n                RateLimitOptions = new()\n                {\n                    ClientIdHeader = \"ClientId\",\n                    QuotaExceededMessage = string.Empty,\n                    RateLimitCounterPrefix = string.Empty,\n                    HttpStatusCode = StatusCodes.Status428PreconditionRequired,\n                },\n                DownstreamScheme = \"http\",\n            },\n        };\n\n        var configuration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                },\n            },\n        };\n        var upstreamPath = $\"/{serviceName}/something\";\n        this.Given(x => x.GivenThereIsAServiceRunningOn(servicePort, \"/something\", HttpStatusCode.OK, \"Hello from Laura\"))\n        .And(x => GivenTheConsulConfigurationIs(consulConfig))\n        .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, serviceName))\n        .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))\n        .And(x => GivenThereIsAConfiguration(configuration))\n        .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig())\n        .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1))\n        .Then(x => ThenTheStatusCodeShouldBe(200))\n        .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 2))\n        .Then(x => ThenTheStatusCodeShouldBe(200))\n        .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1))\n        .Then(x => ThenTheStatusCodeShouldBe(428))\n        .BDDfy();\n    }\n\n    private async Task ThenTheConfigIsUpdatedInOcelot()\n    {\n        var result = await Wait.For(20_000).UntilAsync(async () =>\n        {\n            try\n            {\n                await WhenIGetUrlOnTheApiGateway(\"/cs/status/awesome\");\n                ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n                ThenTheResponseBodyShouldBe(\"Hello from Laura\");\n                return true;\n            }\n            catch (Exception)\n            {\n                return false;\n            }\n        });\n        result.ShouldBeTrue();\n    }\n\n    private void GivenTheConsulConfigurationIs(FileConfiguration config)\n    {\n        _config = config;\n    }\n\n    private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)\n    {\n        foreach (var serviceEntry in serviceEntries)\n        {\n            _consulServices.Add(serviceEntry);\n        }\n    }\n\n    private Task GivenOcelotIsRunningUsingConsulToStoreConfig()\n    {\n        static void WithConsulToStoreConfig(IServiceCollection services)\n            => services.AddOcelot().AddConsul().AddConfigStoredInConsul();\n        GivenOcelotIsRunning(WithConsulToStoreConfig);\n        return Task.Delay(1000);\n    }\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port, string serviceName)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, async context =>\n        {\n            if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.CurrentCultureIgnoreCase) && context.Request.Path.Value == \"/v1/kv/InternalConfiguration\")\n            {\n                var json = JsonConvert.SerializeObject(_config);\n                var bytes = Encoding.UTF8.GetBytes(json);\n                var base64 = Convert.ToBase64String(bytes);\n                var kvp = new FakeConsulGetResponse(base64);\n                json = JsonConvert.SerializeObject(new[] { kvp });\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n            }\n            else if (context.Request.Method.Equals(HttpMethods.Put, StringComparison.CurrentCultureIgnoreCase) && context.Request.Path.Value == \"/v1/kv/InternalConfiguration\")\n            {\n                try\n                {\n                    var reader = new StreamReader(context.Request.Body);\n\n                    // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.\n                    // var json = reader.ReadToEnd();                                            \n                    var json = await reader.ReadToEndAsync();\n                    _config = JsonConvert.DeserializeObject<FileConfiguration>(json);\n                    var response = JsonConvert.SerializeObject(true);\n                    await context.Response.WriteAsync(response);\n                }\n                catch (Exception e)\n                {\n                    Console.WriteLine(e);\n                    throw;\n                }\n            }\n            else if (context.Request.Path.Value == $\"/v1/health/service/{serviceName}\")\n            {\n                var json = JsonConvert.SerializeObject(_consulServices);\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n            }\n        });\n    }\n\n    public class FakeConsulGetResponse\n    {\n        public FakeConsulGetResponse(string value) => Value = value;\n\n        public int CreateIndex => 100;\n        public int ModifyIndex => 200;\n        public int LockIndex => 200;\n        public string Key => \"InternalConfiguration\";\n        public int Flags => 0;\n        public string Value { get; }\n        public string Session => \"adf4238a-882b-9ddc-4a9d-5b6758e4159e\";\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\n    {\n        Task MapStatus(HttpContext context)\n        {\n            context.Response.StatusCode = (int)statusCode;\n            return context.Response.WriteAsync(responseBody);\n        }\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatus);\n    }\n\n    private class FakeCache : IOcelotCache<FileConfiguration>\n    {\n        public FileConfiguration Get(string key, string region) => throw new NotImplementedException();\n        public void ClearRegion(string region) => throw new NotImplementedException();\n        public bool TryGetValue(string key, string region, out FileConfiguration value) => throw new NotImplementedException();\n        public bool Add(string key, FileConfiguration value, string region, TimeSpan ttl) => throw new NotImplementedException();\n        public FileConfiguration AddOrUpdate(string key, FileConfiguration value, string region, TimeSpan ttl) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulIntegrationTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing System.Runtime.CompilerServices;\nusing ConsulProvider = Ocelot.Provider.Consul.Consul;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n// [Collection(nameof(SequentialTests))]\npublic class ConsulIntegrationTests : Steps\n{\n    private readonly int _consulPort;\n    private readonly string _consulHost;\n    private readonly string _consulScheme;\n    private readonly List<ServiceEntry> _consulServiceEntries;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IHttpContextAccessor> _contextAccessor;\n    private IConsulClientFactory _clientFactory;\n    private IConsulServiceBuilder _serviceBuilder;\n    private ConsulRegistryConfiguration _config;\n    private ConsulProvider _provider;\n    private string _receivedToken;\n\n    public ConsulIntegrationTests()\n    {\n        _consulPort = PortFinder.GetRandomPort();\n        _consulHost = \"localhost\";\n        _consulScheme = Uri.UriSchemeHttp;\n        _consulServiceEntries = new List<ServiceEntry>();\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _contextAccessor = new Mock<IHttpContextAccessor>();\n        _factory.Setup(x => x.CreateLogger<ConsulProvider>()).Returns(_logger.Object);\n        _factory.Setup(x => x.CreateLogger<PollConsul>()).Returns(_logger.Object);\n        _factory.Setup(x => x.CreateLogger<DefaultConsulServiceBuilder>()).Returns(_logger.Object);\n    }\n\n    private void Arrange([CallerMemberName] string serviceName = null)\n    {\n        _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _consulPort, serviceName, null);\n        var context = new DefaultHttpContext();\n        context.Items.Add(nameof(ConsulRegistryConfiguration), _config);\n        _contextAccessor.SetupGet(x => x.HttpContext).Returns(context);\n        _clientFactory = new ConsulClientFactory();\n        _serviceBuilder = new DefaultConsulServiceBuilder(_contextAccessor.Object, _clientFactory, _factory.Object);\n        _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder);\n    }\n\n    [Fact]\n    public async Task Should_return_service_from_consul()\n    {\n        Arrange();\n        var service1 = GivenService(PortFinder.GetRandomPort());\n        _consulServiceEntries.Add(service1.ToServiceEntry());\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().Count.ShouldBe(1);\n    }\n\n    [Fact]\n    public async Task Should_use_token()\n    {\n        Arrange();\n        const string token = \"test token\";\n        var service1 = GivenService(PortFinder.GetRandomPort());\n        _consulServiceEntries.Add(service1.ToServiceEntry());\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n        var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _consulPort, nameof(Should_use_token), token);\n        _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder);\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().Count.ShouldBe(1);\n        _receivedToken.ShouldBe(token);\n    }\n\n    [Fact]\n    public async Task Should_not_return_services_with_invalid_address()\n    {\n        Arrange();\n        var service1 = GivenService(PortFinder.GetRandomPort(), \"http://localhost\");\n        var service2 = GivenService(PortFinder.GetRandomPort(), \"http://localhost\");\n        _consulServiceEntries.Add(service1.ToServiceEntry());\n        _consulServiceEntries.Add(service2.ToServiceEntry());\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().Count.ShouldBe(0);\n        ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning();\n    }\n\n    [Fact]\n    public async Task Should_not_return_services_with_empty_address()\n    {\n        Arrange();\n        var service1 = GivenService(PortFinder.GetRandomPort()).WithAddress(string.Empty);\n        var service2 = GivenService(PortFinder.GetRandomPort()).WithAddress(null);\n        _consulServiceEntries.Add(service1.ToServiceEntry());\n        _consulServiceEntries.Add(service2.ToServiceEntry());\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().Count.ShouldBe(0);\n        ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning();\n    }\n\n    [Fact]\n    public async Task Should_not_return_services_with_invalid_port()\n    {\n        Arrange();\n        var service1 = GivenService(-1);\n        var service2 = GivenService(0);\n        _consulServiceEntries.Add(service1.ToServiceEntry());\n        _consulServiceEntries.Add(service2.ToServiceEntry());\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().Count.ShouldBe(0);\n        ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning();\n    }\n\n    [Fact]\n    public async Task GetAsync_NoEntries_ShouldLogWarning()\n    {\n        Arrange();\n        _consulServiceEntries.Clear(); // NoEntries\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>())).Verifiable();\n        GivenThereIsAFakeConsulServiceDiscoveryProvider();\n\n        // Act\n        var actual = await _provider.GetAsync();\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeEmpty();\n        var expected = $\"Consul Provider: No service entries found for '{nameof(GetAsync_NoEntries_ShouldLogWarning)}' service!\";\n        _logger.Verify(x => x.LogWarning(It.Is<Func<string>>(y => y.Invoke() == expected)), Times.Once);\n    }\n\n    private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new()\n    {\n        Service = serviceName,\n        Address = address ?? \"localhost\",\n        Port = port,\n        ID = id ?? Guid.NewGuid().ToString(),\n        Tags = tags ?? Array.Empty<string>(),\n    };\n\n    private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning()\n    {\n        foreach (var entry in _consulServiceEntries)\n        {\n            var service = entry.Service;\n            var expected = $\"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0.\";\n            _logger.Verify(x => x.LogWarning(It.Is<Func<string>>(y => y.Invoke() == expected)), Times.Once);\n        }\n    }\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider([CallerMemberName] string serviceName = \"test\")\n    {\n        string url = $\"{_consulScheme}://{_consulHost}:{_consulPort}\";\n        handler.GivenThereIsAServiceRunningOn(url, async context =>\n        {\n            if (context.Request.Path.Value == $\"/v1/health/service/{serviceName}\")\n            {\n                if (context.Request.Headers.TryGetValue(\"X-Consul-Token\", out var values))\n                {\n                    _receivedToken = values.First();\n                }\n\n                var json = JsonConvert.SerializeObject(_consulServiceEntries);\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Net.Http.Headers;\nusing Newtonsoft.Json;\nusing Ocelot.AcceptanceTests.LoadBalancer;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Infrastructure;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.ServiceDiscovery.Providers;\nusing System.Net.Http.Headers;\nusing System.Runtime.CompilerServices;\nusing System.Text.RegularExpressions;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n/// <summary>\n/// Tests for the <see cref=\"Provider.Consul.Consul\"/> provider.\n/// </summary>\npublic sealed partial class ConsulServiceDiscoveryTests : ConcurrentSteps, IDisposable\n{\n    private readonly ServiceHandler _consulHandler;\n    private readonly List<ServiceEntry> _consulServices;\n    private readonly List<Node> _consulNodes;\n\n    private string _receivedToken;\n\n    private volatile int _counterConsul;\n    private volatile int _counterNodes;\n\n    public ConsulServiceDiscoveryTests()\n    {\n        _consulHandler = new ServiceHandler();\n        _consulServices = new List<ServiceEntry>();\n        _consulNodes = new List<Node>();\n    }\n\n    public override void Dispose()\n    {\n        _consulHandler?.Dispose();\n        base.Dispose();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"28\")]\n    public void ShouldDiscoverServicesInConsulAndLoadBalanceByLeastConnectionWhenConfigInRoute()\n    {\n        const string serviceName = \"product\";\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(2);\n        var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray();\n        var route = GivenDiscoveryRoute(serviceName: serviceName, loadBalancerType: nameof(LeastConnection));\n        var configuration = GivenServiceDiscovery(consulPort, route);\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 50))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*25*/24, /*25*/26)) // TODO Check strict assertion\n            .BDDfy();\n    }\n\n    private static readonly string[] VersionV1Tags = new[] { \"version-v1\" };\n    private static readonly string[] GetVsOptionsMethods = new[] { \"Get\", \"Options\" };\n\n    [Fact]\n    [Trait(\"Feat\", \"201\")]\n    [Trait(\"Bug\", \"213\")]\n    public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequest()\n    {\n        const string serviceName = \"web\";\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n        var serviceEntryOne = GivenServiceEntry(servicePort, \"localhost\", \"web_90_0_2_224_8080\", VersionV1Tags, serviceName);\n        var route = GivenDiscoveryRoute(\"/api/home\", \"/home\", serviceName, httpMethods: GetVsOptionsMethods);\n        var configuration = GivenServiceDiscovery(consulPort, route);\n        this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), \"/api/home\", \"Hello from Laura\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/home\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"213\")]\n    [Trait(\"Feat\", \"201\")]\n    [Trait(\"Feat\", \"340\")]\n    public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequestWhenDynamicRoutingWithNoRoutes()\n    {\n        const string serviceName = \"web\";\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n        var serviceEntry = GivenServiceEntry(servicePort, \"localhost\", \"web_90_0_2_224_8080\", VersionV1Tags, serviceName);\n\n        var configuration = GivenServiceDiscovery(consulPort);\n        configuration.GlobalConfiguration.DownstreamScheme = \"http\";\n        configuration.GlobalConfiguration.HttpHandlerOptions = new()\n        {\n            AllowAutoRedirect = true,\n            UseCookieContainer = true,\n            UseTracing = false,\n        };\n\n        this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), \"/something\", \"Hello from Laura\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/web/something\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"340\")]\n    public void ShouldUseConsulServiceDiscoveryAndLoadBalanceRequestWhenDynamicRoutingWithNoRoutes()\n    {\n        const string serviceName = \"product\";\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(2);\n        var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray();\n\n        var configuration = GivenServiceDiscovery(consulPort);\n        configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(LeastConnection) };\n        configuration.GlobalConfiguration.DownstreamScheme = \"http\";\n\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently($\"/{serviceName}/\", 50))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*25*/24, /*25*/26)) // TODO Check strict assertion\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"295\")]\n    public void ShouldUseAclTokenToMakeRequestToConsul()\n    {\n        const string serviceName = \"web\";\n        const string token = \"abctoken\";\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n        var serviceEntry = GivenServiceEntry(servicePort, \"localhost\", \"web_90_0_2_224_8080\", VersionV1Tags, serviceName);\n        var route = GivenDiscoveryRoute(\"/api/home\", \"/home\", serviceName, httpMethods: GetVsOptionsMethods);\n\n        var configuration = GivenServiceDiscovery(consulPort, route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token;\n\n        this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), \"/api/home\", \"Hello from Laura\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/home\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .And(x => x.ThenTheTokenIs(token))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"181\")]\n    public void ShouldSendRequestToServiceAfterItBecomesAvailableInConsul()\n    {\n        const string serviceName = \"product\";\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(2);\n        var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray();\n        var route = GivenDiscoveryRoute(serviceName: serviceName);\n        var configuration = GivenServiceDiscovery(consulPort, route);\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(_ => GivenMultipleServiceInstancesAreRunning(urls, serviceName))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .And(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 10))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(10))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*5*/4, /*5*/6)) // TODO Check strict assertion\n            .And(x => x.WhenIRemoveAService(serviceEntries[1])) // 2nd entry\n            .And(x => x.GivenIResetCounters())\n            .And(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 10))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(10, 0)) // 2nd is offline\n            .And(x => x.WhenIAddAServiceBackIn(serviceEntries[1])) // 2nd entry\n            .And(x => x.GivenIResetCounters())\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 10))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(10))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*5*/4, /*5*/6)) // TODO Check strict assertion\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"374\")]\n    public void ShouldPollConsulForDownstreamServiceAndMakeRequest()\n    {\n        const string serviceName = \"web\";\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort();\n        var serviceEntry = GivenServiceEntry(servicePort, \"localhost\", $\"web_90_0_2_224_{servicePort}\", VersionV1Tags, serviceName);\n        var route = GivenDiscoveryRoute(\"/api/home\", \"/home\", serviceName, httpMethods: GetVsOptionsMethods);\n        var configuration = GivenServiceDiscovery(consulPort, route);\n\n        var sd = configuration.GlobalConfiguration.ServiceDiscoveryProvider;\n        sd.Type = nameof(PollConsul); // !!!\n        sd.PollingInterval = 0;\n        sd.Namespace = string.Empty;\n\n        this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), \"/api/home\", \"Hello from Laura\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(\"/home\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    private async Task WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url)\n    {\n        var result = await Wait.For(2_000).UntilAsync(async () =>\n        {\n            try\n            {\n                response = await ocelotClient.GetAsync(url);\n                response.EnsureSuccessStatusCode();\n                return true;\n            }\n            catch (Exception)\n            {\n                return false;\n            }\n        });\n        result.ShouldBeTrue();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"849\")]\n    [Trait(\"Bug\", \"1496\")]\n    [Trait(\"PR\", \"1944\")]\n    [InlineData(nameof(NoLoadBalancer))]\n    [InlineData(nameof(RoundRobin))]\n    [InlineData(nameof(LeastConnection))]\n    [InlineData(nameof(CookieStickySessions))]\n    public void ShouldUseConsulServiceDiscoveryWhenThereAreTwoUpstreamHosts(string loadBalancerType)\n    {\n        // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets)\n        // with different ServiceNames (e.g. product-us and product-eu),\n        // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) \n        const string serviceNameUS = \"product-us\";\n        const string serviceNameEU = \"product-eu\";\n        string[] tagsUS = new[] { \"US\" }, tagsEU = new[] { \"EU\" };\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePortUS = PortFinder.GetRandomPort();\n        var servicePortEU = PortFinder.GetRandomPort();\n        const string upstreamHostUS = \"us-shop\";\n        const string upstreamHostEU = \"eu-shop\";\n        const string responseBodyUS = \"Phone chargers with US plug\";\n        const string responseBodyEU = \"Phone chargers with EU plug\";\n        var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: tagsUS);\n        var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: tagsEU);\n        var routeUS = GivenDiscoveryRoute(\"/products\", \"/\", serviceNameUS, loadBalancerType, upstreamHostUS);\n        var routeEU = GivenDiscoveryRoute(\"/products\", \"/\", serviceNameEU, loadBalancerType, upstreamHostEU);\n        var configuration = GivenServiceDiscovery(consulPort, routeUS, routeEU);\n        bool isStickySession = loadBalancerType == nameof(CookieStickySessions);\n        var sessionCookieUS = isStickySession ? new CookieHeaderValue(routeUS.LoadBalancerOptions.Key, Guid.NewGuid().ToString()) : null;\n        var sessionCookieEU = isStickySession ? new CookieHeaderValue(routeEU.LoadBalancerOptions.Key, Guid.NewGuid().ToString()) : null;\n\n        // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return \"Phone chargers with US plug\"\n        // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return \"Phone chargers with EU plug\"\n        this.Given(x => handler.GivenThereIsAServiceRunningOn(servicePortUS, \"/products\", MapGet(\"/products\", responseBodyUS)))\n            .Given(x => handler.GivenThereIsAServiceRunningOn(servicePortEU, \"/products\", MapGet(\"/products\", responseBodyEU)))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeUS.UpstreamPathTemplate, upstreamHostUS, sessionCookieUS),\n                    \"When I get US shop for the first time\")\n            .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1))\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(responseBodyUS))\n            .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeEU.UpstreamPathTemplate, upstreamHostEU, sessionCookieEU),\n                    \"When I get EU shop for the first time\")\n            .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2))\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(responseBodyEU))\n            .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeUS.UpstreamPathTemplate, upstreamHostUS, sessionCookieUS),\n                    \"When I get US shop again\")\n            .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(isStickySession ? 2 : 3)) // sticky sessions use cache, so Consul shouldn't be called\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(responseBodyUS))\n            .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeEU.UpstreamPathTemplate, upstreamHostEU, sessionCookieEU),\n                    \"When I get EU shop again\")\n            .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(isStickySession ? 2 : 4)) // sticky sessions use cache, so Consul shouldn't be called\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(responseBodyEU))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"954\")]\n    public void ShouldReturnServiceAddressByOverriddenServiceBuilderWhenThereIsANode()\n    {\n        const string serviceName = \"OpenTestService\";\n        string[] methods = new[] { HttpMethods.Post, HttpMethods.Get };\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort = PortFinder.GetRandomPort(); // 9999\n        var serviceEntry = GivenServiceEntry(servicePort,\n            id: \"OPEN_TEST_01\",\n            serviceName: serviceName,\n            tags: new[] { serviceName });\n        var serviceNode = new Node() { Name = \"n1\" }; // cornerstone of the bug\n        serviceEntry.Node = serviceNode;\n        var route = GivenDiscoveryRoute(\"/api/{url}\", \"/open/{url}\", serviceName, httpMethods: methods);\n        var configuration = GivenServiceDiscovery(consulPort, route);\n\n        this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), \"/api/home\", \"Hello from Raman\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry))\n            .And(x => x.GivenTheServiceNodesAreRegisteredWithConsul(serviceNode))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul)) // default services registration results with the bug: \"n1\" host issue\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/open/home\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\n            .And(x => ThenTheResponseBodyShouldBe(\"\"))\n            .And(x => ThenConsulShouldHaveBeenCalledTimes(1))\n            .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(1))\n\n            // Override default service builder\n            .Given(x => GivenOcelotIsRunning(WithConsulServiceBuilder))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/open/home\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Raman\"))\n            .And(x => ThenConsulShouldHaveBeenCalledTimes(2))\n            .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(2))\n            .BDDfy();\n    }\n\n    private static readonly string[] Bug2119ServiceNames = new string[] { \"ProjectsService\", \"CustomersService\" };\n    private readonly ILoadBalancer[] _lbAnalyzers = new ILoadBalancer[Bug2119ServiceNames.Length]; // emulate LoadBalancerHouse's collection\n\n    private TLoadBalancer GetAnalyzer<TLoadBalancer, TLoadBalancerCreator>(DownstreamRoute route, IServiceDiscoveryProvider provider)\n        where TLoadBalancer : class, ILoadBalancer\n        where TLoadBalancerCreator : class, ILoadBalancerCreator, new()\n    {\n        //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse\n        int index = Array.IndexOf(Bug2119ServiceNames, route.ServiceName); // LoadBalancerHouse should return different balancers for different service names\n        _lbAnalyzers[index] ??= new TLoadBalancerCreator().Create(route, provider)?.Data;\n        return (TLoadBalancer)_lbAnalyzers[index];\n    }\n\n    private void WithLbAnalyzer<TLoadBalancer, TLoadBalancerCreator>(IServiceCollection services)\n        where TLoadBalancer : class, ILoadBalancer\n        where TLoadBalancerCreator : class, ILoadBalancerCreator, new()\n        => services.AddOcelot().AddConsul().AddCustomLoadBalancer(GetAnalyzer<TLoadBalancer, TLoadBalancerCreator>);\n\n    [Theory]\n    [Trait(\"Bug\", \"2119\")]\n    [InlineData(nameof(NoLoadBalancer))]\n    [InlineData(nameof(RoundRobin))]\n    [InlineData(nameof(LeastConnection))] // original scenario\n    public void ShouldReturnDifferentServicesWhenThereAre2SequentialRequestsToDifferentServices(string loadBalancer)\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length);\n        var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]);\n        var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]);\n        var route1 = GivenDiscoveryRoute(\"/{all}\", \"/projects/{all}\", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer);\n        var route2 = GivenDiscoveryRoute(\"/{all}\", \"/customers/{all}\", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer);\n        route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete };\n        var configuration = GivenServiceDiscovery(consulPort, route1, route2);\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n\n            // Step 1\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/projects/api/projects\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenServiceShouldHaveBeenCalledTimes(0, 1))\n            .And(x => x.ThenTheResponseBodyShouldBe($\"1^:^{Bug2119ServiceNames[0]}\")) // !\n\n            // Step 2\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/customers/api/customers\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenServiceShouldHaveBeenCalledTimes(1, 1))\n            .And(x => x.ThenTheResponseBodyShouldBe($\"1^:^{Bug2119ServiceNames[1]}\")) // !!\n\n            // Finally\n            .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(2))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(1, 1))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2119\")]\n    [InlineData(false, nameof(NoLoadBalancer))]\n    [InlineData(false, nameof(LeastConnection))] // original scenario, clean config\n    [InlineData(true, nameof(LeastConnectionAnalyzer))] // extended scenario using analyzer\n    [InlineData(false, nameof(RoundRobin))]\n    [InlineData(true, nameof(RoundRobinAnalyzer))]\n    public void ShouldReturnDifferentServicesWhenSequentiallyRequestingToDifferentServices(bool withAnalyzer, string loadBalancer)\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length);\n        var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]);\n        var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]);\n        var route1 = GivenDiscoveryRoute(\"/{all}\", \"/projects/{all}\", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer);\n        var route2 = GivenDiscoveryRoute(\"/{all}\", \"/customers/{all}\", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer);\n        route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete };\n        var configuration = GivenServiceDiscovery(consulPort, route1, route2);\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        Func<int, Task> requestToProjectsAndThenRequestToCustomersAndAssert = async (i) =>\n        {\n            // Step 1\n            int count = i + 1;\n            await WhenIGetUrlOnTheApiGateway(\"/projects/api/projects\");\n            ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n            ThenServiceShouldHaveBeenCalledTimes(0, count);\n            ThenTheResponseBodyShouldBe($\"{count}^:^{Bug2119ServiceNames[0]}\", $\"i is {i}\");\n            _responses[2 * i] = response;\n\n            // Step 2\n            await WhenIGetUrlOnTheApiGateway(\"/customers/api/customers\");\n            ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n            ThenServiceShouldHaveBeenCalledTimes(1, count);\n            ThenTheResponseBodyShouldBe($\"{count}^:^{Bug2119ServiceNames[1]}\", $\"i is {i}\");\n            _responses[(2 * i) + 1] = response;\n        };\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) // service names as responses\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(withAnalyzer ? WithLbAnalyzer(loadBalancer) : WithConsul))\n            .When(x => WhenIDoActionMultipleTimes(50, requestToProjectsAndThenRequestToCustomersAndAssert))\n            .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK))\n            .And(x => x.ThenResponsesShouldHaveBodyFromDifferentServices(ports, Bug2119ServiceNames)) // !!!\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(100))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(50, 50))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 50)) // strict assertion\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2119\")]\n    [InlineData(false, nameof(NoLoadBalancer))]\n    [InlineData(false, nameof(LeastConnection))] // original scenario, clean config\n    [InlineData(true, nameof(LeastConnectionAnalyzer))] // extended scenario using analyzer\n    [InlineData(false, nameof(RoundRobin))]\n    [InlineData(true, nameof(RoundRobinAnalyzer))]\n    public void ShouldReturnDifferentServicesWhenConcurrentlyRequestingToDifferentServices(bool withAnalyzer, string loadBalancer)\n    {\n        const int total = 100; // concurrent requests\n        var consulPort = PortFinder.GetRandomPort();\n        var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length);\n        var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]);\n        var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]);\n        var route1 = GivenDiscoveryRoute(\"/{all}\", \"/projects/{all}\", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer);\n        var route2 = GivenDiscoveryRoute(\"/{all}\", \"/customers/{all}\", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer);\n        route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete };\n        var configuration = GivenServiceDiscovery(consulPort, route1, route2);\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) // service names as responses\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(withAnalyzer ? WithLbAnalyzer(loadBalancer) : WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(total, \"/projects/api/projects\", \"/customers/api/customers\"))\n            .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK))\n            .And(x => x.ThenResponsesShouldHaveBodyFromDifferentServices(ports, Bug2119ServiceNames)) // !!!\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(total))\n            .And(x => ThenServiceCountersShouldMatchLeasingCounters((ILoadBalancerAnalyzer)_lbAnalyzers[0], ports, 50)) // ProjectsService\n            .And(x => ThenServiceCountersShouldMatchLeasingCounters((ILoadBalancerAnalyzer)_lbAnalyzers[1], ports, 50)) // CustomersService\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(total, ports.Length), Top(total, ports.Length)))\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 50)) // strict assertion\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes()\n    {\n        var ports = PortFinder.GetPorts(5);\n        var serviceName = ServiceName();\n        var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray();\n        var consulPort = PortFinder.GetRandomPort();\n        var configuration = GivenServiceDiscovery(consulPort);\n        configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin));\n        configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp;\n        configuration.Routes = []; // dynamic routing\n        configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes\n\n        var urls = ports.Select(DownstreamUrl).ToArray();\n        this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort)))\n            .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithConsul))\n            .When(x => WhenIGetUrlOnTheApiGatewayConcurrently($\"/{serviceName}/\", 50))\n            .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50))\n            .And(x => ThenAllServicesCalledRealisticAmountOfTimes(9, 11)) // soft assertion\n            .And(x => ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10)) // distribution by RoundRobin algorithm, aka strict assertion\n            .BDDfy();\n    }\n\n    private Action<IServiceCollection> WithLbAnalyzer(string loadBalancer) => loadBalancer switch\n    {\n        nameof(LeastConnection) => WithLbAnalyzer<LeastConnection, LeastConnectionCreator>,\n        nameof(LeastConnectionAnalyzer) => WithLbAnalyzer<LeastConnectionAnalyzer, LeastConnectionAnalyzerCreator>,\n        nameof(RoundRobin) => WithLbAnalyzer<RoundRobin, RoundRobinCreator>,\n        nameof(RoundRobinAnalyzer) => WithLbAnalyzer<RoundRobinAnalyzer, RoundRobinAnalyzerCreator>,\n        _ => WithLbAnalyzer<LeastConnection, LeastConnectionCreator>,\n    };\n\n    private void ThenResponsesShouldHaveBodyFromDifferentServices(int[] ports, string[] serviceNames)\n    {\n        foreach (var response in _responses)\n        {\n            var headers = response.Value.Headers;\n            headers.TryGetValues(HeaderNames.ServiceIndex, out var indexValues).ShouldBeTrue();\n            int serviceIndex = int.Parse(indexValues.FirstOrDefault() ?? \"-1\");\n            serviceIndex.ShouldBeGreaterThanOrEqualTo(0);\n\n            headers.TryGetValues(HeaderNames.Host, out var hostValues).ShouldBeTrue();\n            hostValues.FirstOrDefault().ShouldBe(\"localhost\");\n            headers.TryGetValues(HeaderNames.Port, out var portValues).ShouldBeTrue();\n            portValues.FirstOrDefault().ShouldBe(ports[serviceIndex].ToString());\n\n            var body = response.Value.Content.ReadAsStringAsync().Result;\n            var serviceName = serviceNames[serviceIndex];\n            body.ShouldNotBeNull().ShouldEndWith(serviceName);\n\n            headers.TryGetValues(HeaderNames.Counter, out var counterValues).ShouldBeTrue();\n            var counter = counterValues.ShouldNotBeNull().FirstOrDefault().ShouldNotBeNull();\n            body.ShouldBe($\"{counter}^:^{serviceName}\");\n        }\n    }\n\n    private static void WithConsul(IServiceCollection services) => services\n        .AddOcelot().AddConsul();\n\n    private static void WithConsulServiceBuilder(IServiceCollection services) => services\n        .AddOcelot().AddConsul<MyConsulServiceBuilder>();\n\n    public class MyConsulServiceBuilder : DefaultConsulServiceBuilder\n    {\n        public MyConsulServiceBuilder(IHttpContextAccessor contextAccessor, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory)\n            : base(contextAccessor, clientFactory, loggerFactory) { }\n\n        protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address;\n    }\n\n    private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new()\n    {\n        Service = new AgentService\n        {\n            Service = serviceName,\n            Address = address ?? \"localhost\",\n            Port = port,\n            ID = id ?? Guid.NewGuid().ToString(),\n            Tags = tags ?? Array.Empty<string>(),\n        },\n    };\n\n    private FileRoute GivenDiscoveryRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new()\n    {\n        DownstreamPathTemplate = downstream ?? \"/\",\n        DownstreamScheme = Uri.UriSchemeHttp,\n        UpstreamPathTemplate = upstream ?? \"/\",\n        UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : [HttpMethods.Get],\n        UpstreamHost = upstreamHost,\n        ServiceName = serviceName,\n        LoadBalancerOptions = new()\n        {\n            Type = loadBalancerType ?? nameof(LeastConnection),\n            Key = serviceName,\n            Expiry = 60_000,\n        },\n    };\n\n    private FileConfiguration GivenServiceDiscovery(int consulPort, params FileRoute[] routes)\n    {\n        var config = GivenConfiguration(routes);\n        config.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = Uri.UriSchemeHttp,\n            Host = \"localhost\",\n            Port = consulPort,\n            Type = nameof(Provider.Consul.Consul),\n        };\n        return config;\n    }\n\n    private async Task WhenIGetUrlOfRequestComingFromHost(string url, string requestHost, CookieHeaderValue cookie)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Get, url);\n        request.Headers.Add(nameof(HttpRequestHeaders.Host), requestHost); // !\n        if (cookie != null)\n            request.Headers.Add(\"Cookie\", cookie.ToString());\n        response = await ocelotClient.ShouldNotBeNull().SendAsync(request);\n    }\n\n    private void ThenTheTokenIs(string token)\n    {\n        _receivedToken.ShouldBe(token);\n    }\n\n    private void WhenIAddAServiceBackIn(ServiceEntry serviceEntry)\n    {\n        _consulServices.Add(serviceEntry);\n    }\n\n    private void WhenIRemoveAService(ServiceEntry serviceEntry)\n    {\n        _consulServices.Remove(serviceEntry);\n    }\n\n    private void GivenIResetCounters()\n    {\n        _counters[0] = _counters[1] = 0;\n        _counterConsul = 0;\n    }\n\n    private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) => _consulServices.AddRange(serviceEntries);\n    private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes);\n\n    [GeneratedRegex(\"/v1/health/service/(?<serviceName>[^/]+)\", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)]\n    private static partial Regex ServiceNameRegex();\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url)\n    {\n        _consulHandler.GivenThereIsAServiceRunningOn(url, async context =>\n        {\n            if (context.Request.Headers.TryGetValue(\"X-Consul-Token\", out var values))\n            {\n                _receivedToken = values.First();\n            }\n\n            // Parse the request path to get the service name\n            var pathMatch = ServiceNameRegex().Match(context.Request.Path.Value);\n            if (pathMatch.Success)\n            {\n                //string json;\n                //lock (ConsulCounterLocker)\n                //{\n                //_counterConsul++;\n                int count = Interlocked.Increment(ref _counterConsul);\n\n                // Use the parsed service name to filter the registered Consul services\n                var serviceName = pathMatch.Groups[\"serviceName\"].Value;\n                var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList();\n                var json = JsonConvert.SerializeObject(services);\n\n                //}\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n                return;\n            }\n\n            if (context.Request.Path.Value == \"/v1/catalog/nodes\")\n            {\n                //_counterNodes++;\n                int count = Interlocked.Increment(ref _counterNodes);\n                var json = JsonConvert.SerializeObject(_consulNodes);\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n            }\n        });\n    }\n\n    private void ThenConsulShouldHaveBeenCalledTimes(int expected) => _counterConsul.ShouldBe(expected);\n    private void ThenConsulNodesShouldHaveBeenCalledTimes(int expected) => _counterNodes.ShouldBe(expected);\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulTwoDownstreamServicesTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class ConsulTwoDownstreamServicesTests : Steps\n{\n    private readonly List<ServiceEntry> _serviceEntries;\n\n    public ConsulTwoDownstreamServicesTests()\n    {\n        _serviceEntries = new List<ServiceEntry>();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"194\")] // https://github.com/ThreeMammals/Ocelot/issues/194\n    public void Should_fix_issue_194()\n    {\n        var consulPort = PortFinder.GetRandomPort();\n        var servicePort1 = PortFinder.GetRandomPort();\n        var servicePort2 = PortFinder.GetRandomPort();\n        var route1 = GivenRoute(servicePort1, \"/api/user/{user}\", \"/api/user/{user}\");\n        var route2 = GivenRoute(servicePort2, \"/api/product/{product}\", \"/api/product/{product}\");\n        var configuration = GivenConfiguration(route1, route2);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = Uri.UriSchemeHttps,\n            Host = \"localhost\",\n            Port = consulPort,\n        };\n        this.Given(x => x.GivenProductServiceIsRunning(servicePort1, \"/api/user/info\", HttpStatusCode.OK, \"user\"))\n            .And(x => x.GivenProductServiceIsRunning(servicePort2, \"/api/product/info\", HttpStatusCode.OK, \"product\"))\n            .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/api/user/info?id=1\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"user\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/api/product/info?id=1\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"product\"))\n            .BDDfy();\n    }\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, context =>\n        {\n            if (context.Request.Path.Value == \"/v1/health/service/product\")\n            {\n                var json = JsonConvert.SerializeObject(_serviceEntries);\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                return context.Response.WriteAsync(json);\n            }\n            return context.Response.WriteAsync(string.Empty);\n        });\n    }\n\n    private void GivenProductServiceIsRunning(int port, string basePath, HttpStatusCode statusCode, string responseBody)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\n        {\n            var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\n            bool oK = downstreamPath == basePath;\n            context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound;\n            return context.Response.WriteAsync(oK ? responseBody : \"downstream path didn't match base path\");\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Newtonsoft.Json;\nusing Ocelot.AcceptanceTests.WebSockets;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Consul;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class ConsulWebSocketTests : WebSocketsSteps\n{\n    private readonly List<ServiceEntry> _serviceEntries = new();\n\n    [Fact]\n    public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer()\n    {\n        var downstreamPort = PortFinder.GetRandomPort();\n        var downstreamHost = \"localhost\";\n\n        var secondDownstreamPort = PortFinder.GetRandomPort();\n        var secondDownstreamHost = \"localhost\";\n\n        var serviceName = \"websockets\";\n        var consulPort = PortFinder.GetRandomPort();\n        var serviceEntryOne = new ServiceEntry\n        {\n            Service = new AgentService\n            {\n                Service = serviceName,\n                Address = downstreamHost,\n                Port = downstreamPort,\n                ID = Guid.NewGuid().ToString(),\n                Tags = Array.Empty<string>(),\n            },\n        };\n        var serviceEntryTwo = new ServiceEntry\n        {\n            Service = new AgentService\n            {\n                Service = serviceName,\n                Address = secondDownstreamHost,\n                Port = secondDownstreamPort,\n                ID = Guid.NewGuid().ToString(),\n                Tags = Array.Empty<string>(),\n            },\n        };\n\n        var config = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    UpstreamPathTemplate = \"/\",\n                    DownstreamPathTemplate = \"/ws\",\n                    DownstreamScheme = \"ws\",\n                    LoadBalancerOptions = new FileLoadBalancerOptions { Type = \"RoundRobin\" },\n                    ServiceName = serviceName,\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Scheme = \"http\",\n                    Host = \"localhost\",\n                    Port = consulPort,\n                    Type = \"consul\",\n                },\n            },\n        };\n        int ocelotPort = PortFinder.GetRandomPort();\n        this.Given(_ => GivenThereIsAConfiguration(config))\n            .And(_ => StartOcelotWithWebSockets(ocelotPort, WithConsul))\n            .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, serviceName))\n            .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))\n            .And(_ => GivenWebSocketsServiceIsRunningAsync(downstreamPort, \"/ws\", EchoAsync, CancellationToken.None))\n            .And(_ => GivenWebSocketsServiceIsRunningAsync(secondDownstreamPort, \"/ws\", MessageAsync, CancellationToken.None))\n            .When(_ => WhenIStartTheClients(ocelotPort))\n            .Then(_ => ThenBothDownstreamServicesAreCalled())\n            .BDDfy();\n    }\n\n    private void WithConsul(IServiceCollection services) => services.AddOcelot().AddConsul();\n\n    private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)\n    {\n        foreach (var serviceEntry in serviceEntries)\n        {\n            _serviceEntries.Add(serviceEntry);\n        }\n    }\n\n    private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port, string serviceName)\n    {\n        Task MapServicePath(HttpContext context)\n        {\n            if (context.Request.Path.Value == $\"/v1/health/service/{serviceName}\")\n            {\n                var json = JsonConvert.SerializeObject(_serviceEntries);\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                return context.Response.WriteAsync(json);\n            }\n            return Task.CompletedTask;\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapServicePath);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/DynamicRoutingTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.AcceptanceTests.Authentication;\nusing Ocelot.AcceptanceTests.Caching;\nusing Ocelot.AcceptanceTests.QualityOfService;\nusing Ocelot.AcceptanceTests.Requester;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Logging;\nusing Ocelot.Metadata;\nusing Ocelot.Provider.Polly;\nusing Ocelot.Requester;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Testing.Authentication;\nusing Ocelot.Values;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n/// <summary>\n/// These tests are based on the custom service discovery provider, abstracting from currently implemented discovery providers and focusing on the dynamic routing features.\n/// </summary>\npublic class DynamicRoutingTests : ConcurrentSteps\n{\n    [Fact]\n    [Trait(\"Feat\", \"351\")]\n    public void ShouldForwardQueryStringToDownstream()\n    {\n        var ports = PortFinder.GetPorts(2);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        GivenMultipleServiceInstancesAreRunning(serviceUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscovery);\n        var pathWithQueryString = $\"/{serviceName}/?{nameof(TestID)}={TestID}\";\n        WhenIGetUrlOnTheApiGatewayConcurrently(pathWithQueryString, 2);\n        ThenAllServicesShouldHaveBeenCalledTimes(2);\n        ThenServicesShouldHaveBeenCalledTimes(1, 1);\n        var pathAndQuery = ThenAllResponsesHeaderExists(HeaderNames.Path).ToList();\n        pathAndQuery.ShouldAllBe(pathQuery => pathWithQueryString.Contains(pathQuery));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes()\n    {\n        var ports = PortFinder.GetPorts(5);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        GivenMultipleServiceInstancesAreRunning(serviceUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscovery);\n        WhenIGetUrlOnTheApiGatewayConcurrently($\"/{serviceName}/\", 50);\n        ThenAllServicesShouldHaveBeenCalledTimes(50);\n        ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion\n        ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void ShouldApplyGlobalGroupLoadBalancerOptions_ForDynamicRoutes_WhenRouteOptsHasAKey()\n    {\n        // 1st route\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(\"route1\", key: null); // 1st route is not in the global group\n        route1.LoadBalancerOptions = null; // 1st route is not balanced\n        GivenDiscoveryMetadata(route1, ports1);\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(\"route2\", key: \"R2\"); // 2nd route is in the group\n        route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones\n        GivenDiscoveryMetadata(route2, ports2);\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(\"noLoadBalancing\", loadBalancer: nameof(NoLoadBalancer), key: null);\n        GivenDiscoveryMetadata(route3, ports3);\n\n        var configuration = GivenDynamicRouting(new(), route1, route2, route3);\n        configuration.GlobalConfiguration.LoadBalancerOptions = new()\n        {\n            RouteKeys = [\"R2\"],\n            Type = nameof(RoundRobin),\n        };\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscovery);\n\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route1/\", 2);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route2/\", 4);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/noLoadBalancing/\", 5);\n        ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below\n        ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2\n        ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2\n        ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4\n        ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4\n        ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5\n        ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")]\n    [Trait(\"PR\", \"2331\")] // https://github.com/ThreeMammals/Ocelot/pull/2331\n    public void ShouldApplyGlobalCacheOptions_ForAllDynamicRoutes()\n    {\n        const int TTL = 1; // let's cache for one second\n        var ports = PortFinder.GetPorts(2);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        configuration.GlobalConfiguration.CacheOptions = new()\n        {\n            TtlSeconds = TTL, // Let's cache for one second\n        };\n\n        var (testBody1, testBody2) = CachingTests.TestBodiesFactory();\n        GivenMultipleServiceInstancesAreRunning(serviceUrls, responses: [testBody1, testBody2]);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscovery);\n        AssertCachedRoute(TTL, serviceName, ports, [testBody1, testBody2]);\n    }\n\n    private void AssertCachedRoute(int ttl, string serviceName, int[] ports, string[] expectedBody, bool cached = true, bool balanced = true, int shift = 0)\n    {\n        Array.Clear(_counters);\n        var url = $\"/{serviceName}/\";\n        WhenIGetUrlOnTheApiGatewayConcurrently(url, 2);\n        ThenAllServicesShouldHaveBeenCalledTimes(2);\n\n        //ThenServicesShouldHaveBeenCalledTimes(1, 1); // distribution by RoundRobin algorithm, aka strict assertion\n        ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? 1 : 2);\n        ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? 1 : 0);\n\n        GivenIWait(100);\n        WhenIGetUrlOnTheApiGatewayConcurrently(url, 2);\n        ThenAllServicesShouldHaveBeenCalledTimes(cached ? 2 : 4); // the counters remain unchanged, and the items are still in the cache\n        int counter = cached ? 1 : 2;\n\n        //ThenServicesShouldHaveBeenCalledTimes(counter, counter); // the counters remain unchanged\n        ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? counter : 2 * counter);\n        ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? counter : 0);\n\n        GivenIWait(ttl * 1000); // allow cached items to expire\n        WhenIGetUrlOnTheApiGatewayConcurrently(url, 2);\n        ThenAllServicesShouldHaveBeenCalledTimes(cached ? 4 : 6); // the counters have been updated because new items were added to the cache\n        counter = cached ? 2 : 3;\n\n        //ThenServicesShouldHaveBeenCalledTimes(counter, counter); // the counters have been updated\n        ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? counter : 2 * counter);\n        ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? counter : 0);\n\n        ThenAllResponseBodiesShouldBe(ports, expectedBody);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")]\n    [Trait(\"PR\", \"2331\")] // https://github.com/ThreeMammals/Ocelot/pull/2331\n    public void ShouldApplyGlobalGroupCacheOptions_WhenRouteOptsHasAKey()\n    {\n        const int TTL = 1; // let's cache for one second\n\n        // 1st route\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(\"route1\", key: null); // 1st route is not in the global group\n        route1.CacheOptions = null; // 1st route is not cached\n        GivenDiscoveryMetadata(route1, ports1);\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(\"route2\", key: \"R2\"); // 2nd route is in the group\n        route2.CacheOptions = null; // 2nd route opts will be applied from global ones\n        GivenDiscoveryMetadata(route2, ports2);\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(\"noCaching\", loadBalancer: nameof(NoLoadBalancer), key: null);\n        GivenDiscoveryMetadata(route3, ports3);\n\n        var configuration = GivenDynamicRouting(new(), route1, route2, route3);\n        configuration.GlobalConfiguration.CacheOptions = new()\n        {\n            RouteKeys = [\"R2\"],\n            Region = \"global\",\n            Header = \"global\",\n            TtlSeconds = TTL,\n        };\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        var (testBody1, testBody2) = CachingTests.TestBodiesFactory();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls, responses: [testBody1, testBody2, testBody1, testBody2, testBody1, testBody2]);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscovery);\n        int length = _counters.Length;\n        AssertCachedRoute(TTL, route1.ServiceName, ports1, [testBody1, testBody2], cached: false, shift: 0);\n        int[] counters1 = new int[length];\n        Array.Copy(_counters, counters1, length);\n\n        AssertCachedRoute(TTL, route2.ServiceName, ports2, [testBody1, testBody2], cached: true, shift: 2);\n        int[] counters2 = new int[length];\n        Array.Copy(_counters, counters2, length);\n\n        AssertCachedRoute(TTL, route3.ServiceName, ports3, [testBody1, testBody2], cached: false, balanced: false, shift: 4);\n        int[] counters3 = new int[length];\n        Array.Copy(_counters, counters3, length);\n\n        for (int i = 0; i < length; i++)\n        {\n            _counters[i] = counters1[i] + counters2[i] + counters3[i];\n        }\n        ThenServicesShouldHaveBeenCalledTimes(3, 3, 2, 2, 6, 0); // main assertion, explanation is below\n        ThenServiceShouldHaveBeenCalledTimes(0, 3); // RoundRobin for 6, not cached\n        ThenServiceShouldHaveBeenCalledTimes(1, 3); // RoundRobin for 6, not cached\n        ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 6, cached 1\n        ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 6, cached 1\n        ThenServiceShouldHaveBeenCalledTimes(4, 6); // NoLoadBalancer for 6, not cached\n        ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 6, not cached\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2320\")]\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\n    public void ShouldApplyGlobalHttpHandlerOptions_ForAllDynamicRoutes()\n    {\n        var ports = PortFinder.GetPorts(3);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        configuration.GlobalConfiguration.HttpHandlerOptions = new()\n        {\n            MaxConnectionsPerServer = 77,\n            PooledConnectionLifetimeSeconds = 88,\n            UseTracing = true, // let's enable global tracing\n        };\n        GivenMultipleServiceInstancesAreRunning(serviceUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscoveryAndRequesterTesting);\n        int times = ports.Length;\n        WhenIGetUrlOnTheApiGatewayConcurrently($\"/{serviceName}/\", times);\n        ThenAllServicesShouldHaveBeenCalledTimes(times);\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 1); // distribution by RoundRobin algorithm, aka strict assertion\n\n        ThenRouteHttpHandlerOptionsAre(serviceName, configuration.GlobalConfiguration.Metadata, 77, 88, true);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2320\")]\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\n    public void ShouldApplyGlobalGroupHttpHandlerOptions_ForDynamicRoutes_WhenRouteOptsHasAKey()\n    {\n        // 1st route\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(\"route1\", key: null); // 1st route is not in the global group\n        route1.HttpHandlerOptions = null; // 1st route has no opts\n        GivenDiscoveryMetadata(route1, ports1);\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(\"route2\", key: \"R2\"); // 2nd route is in the group\n        route2.HttpHandlerOptions = null; // 2nd route opts will be applied from global ones\n        GivenDiscoveryMetadata(route2, ports2);\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(\"noTracing\", loadBalancer: nameof(NoLoadBalancer), key: null);\n        var route3Opts = route3.HttpHandlerOptions = new()\n        {\n            MaxConnectionsPerServer = 66,\n            PooledConnectionLifetimeSeconds = 77,\n            UseTracing = false, // no tracing route\n        };\n        GivenDiscoveryMetadata(route3, ports3);\n\n        var configuration = GivenDynamicRouting(new(), route1, route2, route3);\n        var globalOpts = configuration.GlobalConfiguration.HttpHandlerOptions = new()\n        {\n            RouteKeys = [\"R2\"],\n            MaxConnectionsPerServer = 88,\n            PooledConnectionLifetimeSeconds = 99,\n            UseCookieContainer = false,\n            UseProxy = false,\n            UseTracing = true, // enable global tracing\n        };\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscoveryAndRequesterTesting);\n\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route1/\", 2);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route2/\", 2);\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/noTracing/\", 2);\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 1, 1, 2, 0);\n\n        ThenRouteHttpHandlerOptionsAre(route1.ServiceName, route1.Metadata,\n            int.MaxValue, HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, false); // default opts\n        ThenRouteHttpHandlerOptionsAre(route2.ServiceName, route2.Metadata,\n            globalOpts.MaxConnectionsPerServer.Value, globalOpts.PooledConnectionLifetimeSeconds.Value, globalOpts.UseTracing.Value); // global opts\n        ThenRouteHttpHandlerOptionsAre(route3.ServiceName, route3.Metadata,\n            route3Opts.MaxConnectionsPerServer.Value, route3Opts.PooledConnectionLifetimeSeconds.Value, route3Opts.UseTracing.Value); // route opts\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n    [Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\n    public async Task ShouldApplyGlobalAuthenticationOptions_ForAllDynamicRoutes()\n    {\n        using var steps = new AuthenticationSteps();\n        var ports = PortFinder.GetPorts(3);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        configuration.GlobalConfiguration.AuthenticationOptions = new(AuthenticationSteps.GivenOptions(false, [\"apiGlobal\"], [JwtBearerDefaults.AuthenticationScheme]));\n\n        GivenMultipleServiceInstancesAreRunning(serviceUrls, Enumerable.Repeat(serviceName, ports.Length).ToArray());\n        steps.GivenThereIsAConfiguration(configuration);\n        steps.GivenOcelotIsRunning(WithDiscoveryAndJwtBearerAuthentication(steps));\n        await steps.GivenThereIsExternalJwtSigningService([\"apiGlobal\"], Xunit.TestContext.Current.CancellationToken);\n        await steps.GivenIHaveAToken(scope: \"apiGlobal\"); //,audience: ocelotClient.BaseAddress.Authority);\n        steps.GivenIHaveAddedATokenToMyRequest();\n\n        int times = ports.Length;\n        ocelotClient ??= steps.OcelotClient;\n        WhenIGetUrlOnTheApiGatewayConcurrently($\"/{serviceName}/\", times);\n        ThenAllServicesShouldHaveBeenCalledTimes(times);\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 1); // distribution by RoundRobin algorithm, aka strict assertion\n        ThenAllStatusCodesShouldBe(HttpStatusCode.OK);\n        ThenAllResponseBodiesShouldBe(serviceName);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n    [Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\n    public async Task ShouldApplyGlobalGroupAuthenticationOptions_ForDynamicRoutes_WhenRouteOptsHasAKey()\n    {\n        using var steps = new AuthenticationSteps();\n\n        // 1st route\n        var ports1 = PortFinder.GetPorts(2);\n        var route1 = GivenLbRoute(\"route1\", key: null); // 1st route is not in the global group\n        route1.AuthenticationOptions = null; // 1st route has no opts\n        GivenDiscoveryMetadata(route1, ports1);\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(\"route2\", key: \"R2\"); // 2nd route is in the group\n        route2.AuthenticationOptions = null; // 2nd route opts will be applied from global ones\n        GivenDiscoveryMetadata(route2, ports2);\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(\"noAuthorization\", loadBalancer: nameof(NoLoadBalancer), key: null);\n        var route3Opts = route3.AuthenticationOptions =\n            AuthenticationSteps.GivenOptions(false, [\"invalid-scope\"], [JwtBearerDefaults.AuthenticationScheme]);\n        GivenDiscoveryMetadata(route3, ports3);\n\n        var configuration = GivenDynamicRouting(new(), route1, route2, route3);\n        var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions\n            = new(AuthenticationSteps.GivenOptions(false, [\"apiGlobal\"], [JwtBearerDefaults.AuthenticationScheme]))\n            {\n                RouteKeys = [\"R2\"],\n            };\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls, Enumerable.Repeat(Body(), downstreamUrls.Length).ToArray());\n        steps.GivenThereIsAConfiguration(configuration);\n        steps.GivenOcelotIsRunning(WithDiscoveryAndJwtBearerAuthentication(steps));\n        await steps.GivenThereIsExternalJwtSigningService([\"api\", \"apiGlobal\", \"Mr.Who\"], Xunit.TestContext.Current.CancellationToken);\n        ocelotClient ??= steps.OcelotClient;\n\n        await steps.GivenIHaveAToken(scope: \"Mr.Who\");\n        steps.GivenIHaveAddedATokenToMyRequest();\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route1/\", 2);\n        ThenAllStatusCodesShouldBe(HttpStatusCode.OK); // auth is switched off and the scope doesn't matter\n        ThenAllResponseBodiesShouldBe(Body());\n\n        await steps.GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]);\n        steps.GivenIHaveAddedATokenToMyRequest();\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/route2/\", 2);\n        ThenAllStatusCodesShouldBe(HttpStatusCode.OK); // global scope has been accepted\n        ThenAllResponseBodiesShouldBe(Body());\n\n        await steps.GivenIHaveAToken(scope: \"Mr.Who\"); // should be different scope of route #3 which is \"invalid-scope\"\n        steps.GivenIHaveAddedATokenToMyRequest();\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/noAuthorization/\", 2);\n        ThenAllStatusCodesShouldBe(HttpStatusCode.Forbidden);\n        ThenAllResponseBodiesShouldBe(\"0\");\n\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 1, 1, 0, 0);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    public async Task ShouldApplyGlobalQosOptions_ForAllDynamicRoutes()\n    {\n        var ports = PortFinder.GetPorts(3);\n        var serviceName = ServiceName();\n        var serviceUrls = ports.Select(DownstreamUrl).ToArray();\n        var configuration = GivenDynamicRouting(new()\n        {\n            { serviceName, serviceUrls },\n        });\n        FileQoSOptions globalOptions = configuration.GlobalConfiguration.QoSOptions = new()\n        {\n            BreakDuration = CircuitBreakerStrategy.LowBreakDuration + 1, // 501\n            MinimumThroughput = 2, // exceptions-errors\n            Timeout = 500, // ms\n        };\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscoveryAndPolly);\n\n        using var steps = new QosSteps(this);\n        _counters = new int[serviceUrls.Length];\n        steps.CounterStrategy = (port) =>\n        {\n            int index = Array.FindIndex(serviceUrls, url => new Uri(url).Port == port);\n            int count = Interlocked.Increment(ref _counters[index]);\n        };\n        await steps.TestRouteCircuitBreaker(ports, $\"/{serviceName}/\", globalOptions, isDiscovery: true); // test global scenario\n        await steps.TestRouteTimeout(ports, $\"/{serviceName}/\", globalOptions);\n        ThenServicesShouldHaveBeenCalledTimes(2, 2, 1);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    public async Task ShouldApplyGlobalQosOptions_ForAllDynamicRoutes_WithGroupedOpts()\n    {\n        const int GlobalTimeout = 1500, GlobalExceptions = 3, GlobalBreakMs = 2000;\n        var ports1 = PortFinder.GetPorts(2);\n\n        // 1st route\n        var route1 = GivenLbRoute(\"route1\", key: null); // 1st route is not in the global group\n        route1.QoSOptions = null; // 1st route has no opts\n        GivenDiscoveryMetadata(route1, ports1);\n\n        // 2nd route\n        var ports2 = PortFinder.GetPorts(2);\n        var route2 = GivenLbRoute(\"route2\", key: \"R2\"); // 2nd route is in the group\n        route2.QoSOptions = null; // 2nd route opts will be applied from global ones\n        GivenDiscoveryMetadata(route2, ports2);\n\n        // 3rd route\n        var ports3 = PortFinder.GetPorts(2);\n        var route3 = GivenLbRoute(\"noCircuitBreaker\", loadBalancer: nameof(NoLoadBalancer), key: null);\n        route3.QoSOptions = new()\n        {\n            MinimumThroughput = 0, // disable Circuit Breaker via disallowing of global opts to substitute\n            BreakDuration = 0,\n            Timeout = GlobalTimeout,\n        };\n        GivenDiscoveryMetadata(route3, ports3);\n\n        var configuration = GivenDynamicRouting(new(), route1, route2, route3);\n        var globalOptions = configuration.GlobalConfiguration.QoSOptions\n            = new(new QoSOptions(GlobalExceptions, GlobalBreakMs))\n            {\n                RouteKeys = [\"R2\"],\n            };\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(WithDiscoveryAndPolly);\n\n        var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray();\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls,\n            Enumerable.Repeat(Body(), downstreamUrls.Length).ToArray(),\n            codes: Enumerable.Repeat(HttpStatusCode.NotFound, ports1.Length)\n                .Concat(Enumerable.Repeat(HttpStatusCode.InternalServerError, ports2.Length))\n                .Concat(Enumerable.Repeat(HttpStatusCode.OK, ports3.Length))\n                .ToArray());\n\n        using var steps = new QosSteps(this);\n        WhenIGetUrlOnTheApiGatewayConcurrently($\"/{route1.ServiceName}/\", 2);\n        ThenAllStatusCodesShouldBe(HttpStatusCode.NotFound); // QoS is switched off and the scope doesn't matter\n        ThenAllResponseBodiesShouldBe(Body());\n\n        steps.CounterStrategy = (port) =>\n        {\n            int index = Array.FindIndex(downstreamUrls, url => new Uri(url).Port == port);\n            int count = Interlocked.Increment(ref _counters[index]);\n        };\n        await steps.TestRouteCircuitBreaker(ports2, $\"/{route2.ServiceName}/\", globalOptions, isDiscovery: true); // test global scenario\n        await steps.TestRouteTimeout(ports3, $\"/{route3.ServiceName}/\", route3.QoSOptions);\n\n        ThenServicesShouldHaveBeenCalledTimes(1, 1, 3, 1, 2, 0);\n    }\n\n    private FileConfiguration GivenDynamicRouting(Dictionary<string, IEnumerable<string>> services, params FileDynamicRoute[] routes)\n    {\n        var config = new FileConfiguration()\n        {\n            DynamicRoutes = new(routes),\n            GlobalConfiguration = new()\n            {\n                DownstreamScheme = Uri.UriSchemeHttp,\n                ServiceDiscoveryProvider = new()\n                {\n                    Type = nameof(DynamicRoutingDiscoveryProvider),\n                    Host = \"doesn't matter for this provider\", // it should not be empty due to DownstreamRouteProviderFactory.Get\n                    Port = 1, // see DownstreamRouteProviderFactory.IsServiceDiscovery\n                },\n                LoadBalancerOptions = new(nameof(RoundRobin)),\n            },\n        };\n        config.GlobalConfiguration.Metadata = services.ToDictionary(x => x.Key, x => x.Value.Csv());\n        return config;\n    }\n\n    private FileDynamicRoute GivenLbRoute(string serviceName, string serviceNamespace = null,\n        string loadBalancer = null, string key = null) => new()\n        {\n            ServiceName = serviceName,\n            ServiceNamespace = serviceNamespace ?? ServiceNamespace(),\n            LoadBalancerOptions = new(loadBalancer ?? nameof(RoundRobin)),\n            Key = key,\n        };\n\n    private static void GivenDiscoveryMetadata(FileDynamicRoute route, int[] ports)\n        => route.Metadata = new Dictionary<string, string>()\n        {\n            { route.ServiceName, ports.Select(DownstreamUrl).Csv() },\n        };\n\n    private static readonly ServiceDiscoveryFinderDelegate DynamicRoutingDiscoveryFinder = (provider, config, route)\n        => new DynamicRoutingDiscoveryProvider(provider, config, route);\n    private static void WithDiscovery(IServiceCollection services) => services\n        .AddSingleton(DynamicRoutingDiscoveryFinder)\n        .AddOcelot();\n    private static void WithDiscoveryAndPolly(IServiceCollection services) => services\n        .AddSingleton(DynamicRoutingDiscoveryFinder)\n        .AddOcelot().AddPolly();\n    private static void WithDiscoveryAndRequesterTesting(IServiceCollection services)\n    {\n        WithDiscovery(services);\n        RequesterSteps.WithRequesterTesting(services, false);\n    }\n    private static Action<IServiceCollection> WithDiscoveryAndJwtBearerAuthentication(AuthenticationSteps steps)\n    {\n        Action<IServiceCollection> ocelotServices = WithDiscovery;\n        void withJwtBearerAuthentication(IServiceCollection services)\n            => steps.WithJwtBearerAuthentication(services, false);\n        ocelotServices += withJwtBearerAuthentication;\n        return ocelotServices;\n    }\n\n    private void ThenRouteHttpHandlerOptionsAre(string serviceName, IDictionary<string, string> metadata,\n        int maxConnections, int seconds, bool useTracing)\n    {\n        var pool = OcelotServices.GetService<IMessageInvokerPool>() as TestMessageInvokerPool;\n        pool.ShouldNotBeNull();\n        var tracer = OcelotServices.GetService<IOcelotTracer>() as TestTracer;\n        tracer.ShouldNotBeNull();\n        foreach (var kv in pool.CreatedHandlers.Where(x => x.Key.ServiceName == serviceName))\n        {\n            var downstream = kv.Key;\n            var httpHandler = kv.Value;\n            httpHandler.MaxConnectionsPerServer.ShouldBe(maxConnections);\n            httpHandler.PooledConnectionLifetime.TotalSeconds.ShouldBe(seconds);\n            downstream.HttpHandlerOptions.UseTracing.ShouldBe(useTracing);\n        }\n        var csvData = metadata[serviceName];\n        var serviceUrls = csvData.Split(',');\n        tracer.Requests.Count.ShouldBe(serviceUrls.Length);\n        foreach (var url in serviceUrls)\n        {\n            var request = tracer.Requests.Keys.SingleOrDefault(k => k.RequestUri.AbsoluteUri.StartsWith(url));\n            (request is not null).ShouldBe(useTracing);\n        }\n    }\n\n    protected override string ServiceNamespace() => nameof(DynamicRoutingTests);\n}\n\npublic class DynamicRoutingDiscoveryProvider : IServiceDiscoveryProvider\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly ServiceProviderConfiguration _config;\n    private readonly DownstreamRoute _downstreamRoute;\n\n    public DynamicRoutingDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute)\n    {\n        _serviceProvider = serviceProvider;\n        _config = config;\n        _downstreamRoute = downstreamRoute;\n    }\n\n    public Task<List<Service>> GetAsync()\n    {\n        if (!_downstreamRoute.MetadataOptions.Metadata.TryGetValue(_downstreamRoute.ServiceName, out var data)\n            || data.IsEmpty()) \n            return Task.FromResult<List<Service>>(new());\n\n        var urls = _downstreamRoute\n            .GetMetadata<string[]>(_downstreamRoute.ServiceName)\n            .Select(x => new Uri(x))\n            .ToList();\n        var services = urls\n            .Select(url => new Service(\n                name: _downstreamRoute.ServiceName,\n                hostAndPort: new(url.Host, url.Port, url.Scheme.IfEmpty(_downstreamRoute.DownstreamScheme)),\n                id: $\"{_downstreamRoute.ServiceNamespace}.{_downstreamRoute.ServiceName}\",\n                version: DateTime.UtcNow.ToString(\"O\"),\n                tags: Enumerable.Empty<string>()))\n            .ToList();\n        return Task.FromResult(services);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Provider.Eureka;\nusing Steeltoe.Common.Discovery;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class EurekaServiceDiscoveryTests : Steps\n{\n    private readonly List<IServiceInstance> _eurekaInstances;\n\n    public EurekaServiceDiscoveryTests()\n    {\n        _eurekaInstances = new List<IServiceInstance>();\n    }\n\n#if NET10_0_OR_GREATER\n    [Theory(Skip = \"TODO Requires upgrade to v4.0 after package upgraded\")]\n#else\n    [Theory]\n#endif\n    [Trait(\"Feat\", \"262\")] // https://github.com/ThreeMammals/Ocelot/issues/262\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task Should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer)\n    {\n        Environment.SetEnvironmentVariable(\"DOTNET_RUNNING_IN_CONTAINER\", dotnetRunningInContainer.ToString());\n        var serviceName = \"product\";\n        var eurekaPort = 8761;\n        var port = PortFinder.GetRandomPort();\n\n        var instanceOne = new FakeEurekaService(serviceName, \"localhost\", port, false,\n            new Uri(DownstreamUrl(port)), new Dictionary<string, string>());\n\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/\",\n                    DownstreamScheme = Uri.UriSchemeHttp,\n                    UpstreamPathTemplate = \"/\",\n                    UpstreamHttpMethod = [HttpMethods.Get],\n                    ServiceName = serviceName,\n                    LoadBalancerOptions = new() { Type = nameof(LeastConnection) },\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    Type = nameof(Eureka),\n                },\n            },\n        };\n\n        GivenEurekaProductServiceOneIsRunning(port, HttpStatusCode.OK);\n        GivenThereIsAFakeEurekaServiceDiscoveryProvider(eurekaPort, serviceName);\n        GivenTheServicesAreRegisteredWithEureka(instanceOne);\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunningWithEureka();\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n        ThenTheResponseBodyShouldBe(nameof(Should_use_eureka_service_discovery_and_make_request));\n    }\n\n    private void GivenOcelotIsRunningWithEureka()\n        => GivenOcelotIsRunning(s => s.AddOcelot().AddEureka());\n\n    private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances)\n    {\n        foreach (var instance in serviceInstances)\n        {\n            _eurekaInstances.Add(instance);\n        }\n    }\n\n    private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(int port, string serviceName)\n    {\n        Task MapEurekaService(HttpContext context)\n        {\n            if (context.Request.Path.Value != \"/eureka/apps/\")\n                return Task.CompletedTask;\n\n            var apps = new List<Application>();\n            foreach (var serviceInstance in _eurekaInstances)\n            {\n                var a = new Application\n                {\n                    name = serviceName,\n                    instance = new List<Instance>\n                    {\n                        new()\n                        {\n                            instanceId = $\"{serviceInstance.Host}:{serviceInstance}\",\n                            hostName = serviceInstance.Host,\n                            app = serviceName,\n                            ipAddr = \"127.0.0.1\",\n                            status = \"UP\",\n                            overriddenstatus = \"UNKNOWN\",\n                            port = new Port {value = serviceInstance.Port, enabled = \"true\"},\n                            securePort = new SecurePort {value = serviceInstance.Port, enabled = \"true\"},\n                            countryId = 1,\n                            dataCenterInfo = new DataCenterInfo {value = \"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo\", name = \"MyOwn\"},\n                            leaseInfo = new LeaseInfo\n                            {\n                                renewalIntervalInSecs = 30,\n                                durationInSecs = 90,\n                                registrationTimestamp = 1457714988223,\n                                lastRenewalTimestamp= 1457716158319,\n                                evictionTimestamp = 0,\n                                serviceUpTimestamp = 1457714988223,\n                            },\n                            metadata = new()\n                            {\n                                value = \"java.util.Collections$EmptyMap\",\n                            },\n                            homePageUrl = $\"{serviceInstance.Host}:{serviceInstance.Port}\",\n                            statusPageUrl = $\"{serviceInstance.Host}:{serviceInstance.Port}\",\n                            healthCheckUrl = $\"{serviceInstance.Host}:{serviceInstance.Port}\",\n                            vipAddress = serviceName,\n                            isCoordinatingDiscoveryServer = \"false\",\n                            lastUpdatedTimestamp = \"1457714988223\",\n                            lastDirtyTimestamp = \"1457714988172\",\n                            actionType = \"ADDED\",\n                        },\n                    },\n                };\n                apps.Add(a);\n            }\n\n            var applications = new EurekaApplications\n            {\n                applications = new Applications\n                {\n                    application = apps,\n                    apps__hashcode = \"UP_1_\",\n                    versions__delta = \"1\",\n                },\n            };\n            var json = JsonConvert.SerializeObject(applications);\n            context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n            return context.Response.WriteAsync(json);\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapEurekaService);\n    }\n\n    private void GivenEurekaProductServiceOneIsRunning(int port, HttpStatusCode statusCode, [CallerMemberName] string responseBody = null)\n    {\n        Task MapStatusAndError(HttpContext context)\n        {\n            try\n            {\n                context.Response.StatusCode = (int)statusCode;\n                return context.Response.WriteAsync(responseBody);\n            }\n            catch (Exception exception)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;\n                return context.Response.WriteAsync(exception.StackTrace);\n            }\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapStatusAndError);\n    }\n}\n\npublic class FakeEurekaService : IServiceInstance\n{\n    public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary<string, string> metadata)\n    {\n        ServiceId = serviceId;\n        Host = host;\n        Port = port;\n        IsSecure = isSecure;\n        Uri = uri;\n        Metadata = metadata;\n    }\n\n    public string ServiceId { get; }\n    public string Host { get; }\n    public int Port { get; }\n    public bool IsSecure { get; }\n    public Uri Uri { get; }\n    public IDictionary<string, string> Metadata { get; }\n}\n\n#pragma warning disable IDE1006 // Naming Styles\npublic class Port\n{\n    [JsonProperty(\"$\")]\n    public int value { get; set; }\n\n    [JsonProperty(\"@enabled\")]\n    public string enabled { get; set; }\n}\npublic class SecurePort\n{\n    [JsonProperty(\"$\")]\n    public int value { get; set; }\n\n    [JsonProperty(\"@enabled\")]\n    public string enabled { get; set; }\n}\npublic class DataCenterInfo\n{\n    [JsonProperty(\"@class\")]\n    public string value { get; set; }\n    public string name { get; set; }\n}\npublic class LeaseInfo\n{\n    public int renewalIntervalInSecs { get; set; }\n    public int durationInSecs { get; set; }\n    public long registrationTimestamp { get; set; }\n    public long lastRenewalTimestamp { get; set; }\n    public int evictionTimestamp { get; set; }\n    public long serviceUpTimestamp { get; set; }\n}\npublic class ValueMetadata\n{\n    [JsonProperty(\"@class\")]\n    public string value { get; set; }\n}\npublic class Instance\n{\n    public string instanceId { get; set; }\n    public string hostName { get; set; }\n    public string app { get; set; }\n    public string ipAddr { get; set; }\n    public string status { get; set; }\n    public string overriddenstatus { get; set; }\n    public Port port { get; set; }\n    public SecurePort securePort { get; set; }\n    public int countryId { get; set; }\n    public DataCenterInfo dataCenterInfo { get; set; }\n    public LeaseInfo leaseInfo { get; set; }\n    public ValueMetadata metadata { get; set; }\n    public string homePageUrl { get; set; }\n    public string statusPageUrl { get; set; }\n    public string healthCheckUrl { get; set; }\n    public string vipAddress { get; set; }\n    public string isCoordinatingDiscoveryServer { get; set; }\n    public string lastUpdatedTimestamp { get; set; }\n    public string lastDirtyTimestamp { get; set; }\n    public string actionType { get; set; }\n}\npublic class Application\n{\n    public string name { get; set; }\n    public List<Instance> instance { get; set; }\n}\npublic class Applications\n{\n    public string versions__delta { get; set; }\n    public string apps__hashcode { get; set; }\n    public List<Application> application { get; set; }\n}\npublic class EurekaApplications\n{\n    public Applications applications { get; set; }\n}\n#pragma warning restore IDE1006 // Naming Styles\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/KubeIntegrationTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n/// <summary>\n/// Contains integration tests.\n/// Move to integration testing, and add at least one \"happy path\" unit test.\n/// </summary>\n// [Collection(nameof(SequentialTests))]\npublic class KubeIntegrationTests : Steps\n{\n    static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings;\n\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n\n    public KubeIntegrationTests()\n    {\n        _factory = new();\n        _logger = new();\n        _factory.Setup(x => x.CreateLogger<Kube>()).Returns(_logger.Object);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"345\")] // https://github.com/ThreeMammals/Ocelot/issues/345\n    [Trait(\"Release\", \"13.2.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.2.0\n    public async Task Should_return_service_from_k8s()\n    {\n        // Arrange\n        var given = GivenClientAndProvider(out var serviceBuilder);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new Service[] { new(nameof(Should_return_service_from_k8s), new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            responseStatusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> receivedToken);\n\n        // Act\n        var services = await given.Provider.GetAsync();\n\n        // Assert\n        services.ShouldNotBeNull().Count.ShouldBe(1);\n        receivedToken.Value.ShouldBe($\"Bearer {nameof(Should_return_service_from_k8s)}\");\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.BadRequest)]\n    [InlineData(HttpStatusCode.Forbidden)]\n    [InlineData(HttpStatusCode.InternalServerError)]\n    [InlineData(HttpStatusCode.NotFound)]\n    [Trait(\"PR\", \"2266\")] // https://github.com/ThreeMammals/Ocelot/pull/2266\n    [Trait(\"Release\", \"24.0.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0\n    public async Task Should_not_return_service_from_k8s_when_k8s_api_returns_error_response(HttpStatusCode expectedStatusCode)\n    {\n        // Arrange\n        var given = GivenClientAndProvider(out var serviceBuilder);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new Service[] { new(nameof(Should_not_return_service_from_k8s_when_k8s_api_returns_error_response), new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            expectedStatusCode,\n            endpoints,\n            out Lazy<string> receivedToken);\n\n        string expectedKubeApiErrorMessage = GetKubeApiErrorMessage(serviceName: given.ProviderOptions.KeyOfServiceInK8s, given.ProviderOptions.KubeNamespace, expectedStatusCode);\n        string expectedLogMessage = $\"Failed to retrieve v1/Endpoints '{given.ProviderOptions.KeyOfServiceInK8s}' in namespace '{given.ProviderOptions.KubeNamespace}': (HTTP.{expectedStatusCode}/Failure/{expectedStatusCode}): {expectedKubeApiErrorMessage}\";\n        _logger.Setup(logger => logger.LogError(It.IsAny<Func<string>>(), It.IsAny<Exception>()))\n            .Callback((Func<string> messageFactory, Exception exception) =>\n            {\n                messageFactory.ShouldNotBeNull();\n\n                string logMessage = messageFactory();\n                logMessage.ShouldNotBeNullOrWhiteSpace();\n\n                // This is a little fragile, as it may change if other entries are logged due to implementation changes.\n                // Unfortunately, the use of a factory delegate for the log message, combined with reuse of Kube's logger for Retry.OperationAsync makes this tricky to test any other way so this is probably the best we can do for now.\n                if (logMessage.StartsWith(\"Ocelot Retry strategy\"))\n                {\n                    return;\n                }\n\n                logMessage.ShouldBe(expectedLogMessage);\n\n                exception.ShouldNotBeNull();\n                KubeApiException kubeApiException = exception.ShouldBeOfType<KubeApiException>();\n                StatusV1 errorResponse = kubeApiException.Status;\n                errorResponse.Status.ShouldBe(StatusV1.FailureStatus);\n                errorResponse.Code.ShouldBe((int)expectedStatusCode);\n                errorResponse.Reason.ShouldBe(expectedStatusCode.ToString());\n                errorResponse.Message.ShouldNotBeNullOrWhiteSpace();\n            })\n            .Verifiable($\"IOcelotLogger.LogError() was not called.\");\n\n        // Act\n        var services = await given.Provider.GetAsync();\n\n        // Assert\n        services.ShouldNotBeNull().Count.ShouldBe(0);\n        receivedToken.Value.ShouldBe($\"Bearer {nameof(Should_not_return_service_from_k8s_when_k8s_api_returns_error_response)}\");\n        _logger.Verify();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2110\")] // https://github.com/ThreeMammals/Ocelot/issues/2110\n    [Trait(\"Release\", \"23.3.4\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.4\n    public async Task Should_return_single_service_from_k8s_during_concurrent_calls()\n    {\n        // Arrange\n        var given = GivenClientAndProvider(out var serviceBuilder);\n        var manualResetEvent = new ManualResetEvent(false);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() =>\n            {\n                manualResetEvent.WaitOne();\n                return new Service[] { new(nameof(Should_return_single_service_from_k8s_during_concurrent_calls), new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) };\n            });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> receivedToken);\n\n        // Act\n        var services = new List<Service>();\n        async Task WhenIGetTheServices() => services = await given.Provider.GetAsync();\n        var getServiceTasks = Task.WhenAll(\n            WhenIGetTheServices(),\n            WhenIGetTheServices());\n        manualResetEvent.Set();\n        await getServiceTasks;\n\n        // Assert\n        receivedToken.Value.ShouldBe($\"Bearer {nameof(Should_return_single_service_from_k8s_during_concurrent_calls)}\");\n        services.ShouldNotBeNull().Count.ShouldBe(1);\n        services.ShouldAllBe(s => s != null);\n    }\n\n    private (IKubeApiClient Client, KubeClientOptions ClientOptions, Kube Provider, KubeRegistryConfiguration ProviderOptions)\n        GivenClientAndProvider(out Mock<IKubeServiceBuilder> serviceBuilder, string namespaces = null, [CallerMemberName] string serviceName = null)\n    {\n        namespaces ??= nameof(KubeIntegrationTests);\n        var kubePort = PortFinder.GetRandomPort();\n        serviceName ??= \"test\" + kubePort;\n        var kubeEndpointUrl = $\"{Uri.UriSchemeHttp}://localhost:{kubePort}\";\n        var options = new KubeClientOptions\n        {\n            AccessToken = serviceName, // \"txpc696iUhbVoudg164r93CxDTrKRVWG\",\n            AllowInsecure = true,\n            ApiEndPoint = new Uri(kubeEndpointUrl),\n            AuthStrategy = KubeAuthStrategy.BearerToken,\n        };\n        IKubeApiClient client = KubeApiClient.Create(options);\n\n        var config = new KubeRegistryConfiguration\n        {\n            KeyOfServiceInK8s = serviceName,\n            KubeNamespace = namespaces,\n        };\n        serviceBuilder = new();\n        var provider = new Kube(config, _factory.Object, client, serviceBuilder.Object);\n        return (client, options, provider, config);\n    }\n\n    protected EndpointsV1 GivenEndpoints(\n        string namespaces = nameof(KubeIntegrationTests),\n        [CallerMemberName] string serviceName = \"test\")\n    {\n        var endpoints = new EndpointsV1\n        {\n            Kind = \"endpoint\",\n            ApiVersion = \"1.0\",\n            Metadata = new ObjectMetaV1\n            {\n                Name = serviceName,\n                Namespace = namespaces,\n            },\n        };\n        var subset = new EndpointSubsetV1();\n        subset.Addresses.Add(new EndpointAddressV1\n        {\n            Ip = \"127.0.0.1\",\n            Hostname = \"localhost\",\n        });\n        subset.Ports.Add(new EndpointPortV1\n        {\n            Port = 80,\n        });\n        endpoints.Subsets.Add(subset);\n        return endpoints;\n    }\n\n    protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(\n        string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy<string> receivedToken)\n        => GivenThereIsAFakeKubeServiceDiscoveryProvider(url, namespaces, serviceName, HttpStatusCode.OK, endpointEntries, out receivedToken);\n\n    protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(string url, string namespaces, string serviceName,\n        HttpStatusCode responseStatusCode, EndpointsV1 endpointEntries, out Lazy<string> receivedToken)\n    {\n        var token = string.Empty;\n        receivedToken = new(() => token);\n        handler.GivenThereIsAServiceRunningOn(url, ProcessKubernetesRequest);\n        Task ProcessKubernetesRequest(HttpContext context)\n        {\n            if (context.Request.Path.Value == $\"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}\")\n            {\n                string responseBody;\n\n                if (context.Request.Headers.TryGetValue(\"Authorization\", out var values))\n                {\n                    token = values.First();\n                }\n\n                if (responseStatusCode == HttpStatusCode.OK)\n                {\n                    responseBody = JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings);\n                }\n                else\n                {\n                    responseBody = JsonConvert.SerializeObject(new StatusV1\n                    {\n                        Message = GetKubeApiErrorMessage(serviceName, namespaces, responseStatusCode),\n                        Reason = responseStatusCode.ToString(),\n                        Code = (int)responseStatusCode,\n                        Status = StatusV1.FailureStatus,\n                    }, JsonSerializerSettings);\n                }\n\n                context.Response.StatusCode = (int)responseStatusCode;\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                return context.Response.WriteAsync(responseBody);\n            }\n\n            return Task.CompletedTask;\n        }\n    }\n\n    private static string GetKubeApiErrorMessage(string serviceName, string kubeNamespace, HttpStatusCode responseStatusCode)\n    {\n        return $\"Failed to retrieve v1/Endpoints '{serviceName}' in namespace '{kubeNamespace}' (HTTP.{responseStatusCode}/Failure/{responseStatusCode}): This is an error response for HTTP status code {(int)responseStatusCode} ('{responseStatusCode}') from the fake Kubernetes API.\";\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing KubeClient.ResourceClients;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Newtonsoft.Json;\nusing Ocelot.AcceptanceTests.LoadBalancer;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class KubernetesServiceDiscoveryTests : ConcurrentSteps\n{\n    private readonly string _kubernetesUrl;\n    private string _receivedToken;\n    private readonly Action<KubeClientOptions> _kubeClientOptionsConfigure;\n\n    public KubernetesServiceDiscoveryTests()\n    {\n        _kubernetesUrl = DownstreamUrl(PortFinder.GetRandomPort());\n        _kubeClientOptionsConfigure = opts =>\n        {\n            opts.ApiEndPoint = new Uri(_kubernetesUrl);\n            opts.AccessToken = \"txpc696iUhbVoudg164r93CxDTrKRVWG\";\n            opts.AuthStrategy = KubeAuthStrategy.BearerToken;\n            opts.AllowInsecure = true;\n        };\n    }\n\n    [Theory]\n    [InlineData(nameof(Kube))]\n    [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304\n    [InlineData(nameof(WatchKube))]\n    public void ShouldReturnServicesFromK8s(string discoveryType)\n    {\n        var servicePort = PortFinder.GetRandomPort();\n        var downstreamUrl = LoopbackLocalhostUrl(servicePort);\n        var downstream = new Uri(downstreamUrl);\n        var subsetV1 = GivenSubsetAddress(downstream);\n        var endpoints = GivenEndpoints(subsetV1);\n        var route = GivenRouteWithServiceName(ServiceName());\n        var configuration = GivenKubeConfiguration(route, discoveryType);\n        string serviceName = ServiceName(), downstreamResponse = serviceName;\n        this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse))\n            .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning(WithKubernetes))\n            .When(_ => GivenWatchReceivedEvent())\n            .When(_ => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe($\"1^:^{downstreamResponse}\"))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(1))\n            .And(x => x.ThenTheTokenIs(\"Bearer txpc696iUhbVoudg164r93CxDTrKRVWG\"))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1967\")]\n    [InlineData(\"\", HttpStatusCode.BadGateway)]\n    [InlineData(\"http\", HttpStatusCode.OK)]\n    public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode)\n    {\n        const string serviceName = \"example-web\";\n        var servicePort = PortFinder.GetRandomPort();\n        var downstreamUrl = LoopbackLocalhostUrl(servicePort);\n        var downstream = new Uri(downstreamUrl);\n        var subsetV1 = GivenSubsetAddress(downstream);\n\n        // Ports[0] -> port(https, 443)\n        // Ports[1] -> port(http, not 80)\n        subsetV1.Ports.Insert(0, new()\n        {\n            Name = \"https\", // This service instance is offline -> BadGateway\n            Port = 443,\n        });\n        var endpoints = GivenEndpoints(subsetV1);\n        var route = GivenRouteWithServiceName();\n        route.DownstreamPathTemplate = \"/{url}\";\n        route.DownstreamScheme = downstreamScheme; // !!! Warning !!! Select port by name as scheme\n        route.UpstreamPathTemplate = \"/api/example/{url}\";\n        route.ServiceName = serviceName; // \"example-web\"\n        var configuration = GivenKubeConfiguration(route, nameof(Kube));\n\n        this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme)))\n            .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning(WithKubernetes))\n            .When(_ => WhenIGetUrlOnTheApiGateway(\"/api/example/1\"))\n            .Then(_ => ThenTheStatusCodeShouldBe(statusCode))\n            .And(_ => ThenTheResponseBodyShouldBe(downstreamScheme == \"http\"\n                    ? \"1^:^\" + nameof(ShouldReturnServicesByPortNameAsDownstreamScheme)\n                    : string.Empty))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(downstreamScheme == \"http\" ? 1 : 0))\n            .And(x => x.ThenTheTokenIs(\"Bearer txpc696iUhbVoudg164r93CxDTrKRVWG\"))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2110\")]\n    [InlineData(1, 30, null)]\n    [InlineData(2, 50, null)]\n    [InlineData(3, 50, null)]\n    [InlineData(4, 50, null)]\n    [InlineData(5, 50, null)]\n    [InlineData(6, 99, null)]\n    [InlineData(7, 99, null)]\n    [InlineData(8, 99, null)]\n    [InlineData(9, 999, null)]\n    [InlineData(10, 999, nameof(Kube))]\n    // [InlineData(10, 999, nameof(PollKube))]\n    [InlineData(10, 999, nameof(WatchKube))]\n    public void ShouldHighlyLoadOnStableKubeProvider_WithRoundRobinLoadBalancing(int totalServices, int totalRequests, string discoveryType)\n    {\n        // Skip in MacOS because the test is very unstable\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // the test is stable in Linux and Windows only\n            return;\n        discoveryType ??= nameof(Kube);\n        int zeroGeneration = 0, k8sCount = totalRequests;\n        int bottom = totalRequests / totalServices,\n            top = totalRequests - (bottom * totalServices) + bottom;\n        var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices, discoveryType);\n        GivenThereIsAFakeKubernetesProvider(endpoints); // stable, services will not be removed from the list\n\n        HighlyLoadOnKubeProviderAndRoundRobinBalancer(discoveryType, totalRequests, zeroGeneration, k8sCount);\n\n        if (discoveryType == nameof(PollKube))\n            return; // TODO\n        ThenAllServicesCalledRealisticAmountOfTimes(bottom, top);\n        ThenServiceCountersShouldMatchLeasingCounters(_roundRobinAnalyzer, servicePorts, totalRequests);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2110\")]\n    [InlineData(5, 50, 1, null)]\n    [InlineData(5, 50, 2, null)]\n    [InlineData(5, 50, 3, null)]\n    [InlineData(5, 50, 4, nameof(Kube))]\n    // [InlineData(5, 50, 4, nameof(PollKube))]\n    [InlineData(5, 50, 4, nameof(WatchKube))]\n    public void ShouldHighlyLoadOnUnstableKubeProvider_WithRoundRobinLoadBalancing(int totalServices, int totalRequests, int k8sGeneration, string discoveryType)\n    {\n        // Skip in MacOS because the test is very unstable\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // the test is stable in Linux and Windows only\n            return;\n        discoveryType ??= nameof(Kube);\n        int failPerThreads = (totalRequests / k8sGeneration) - 1, // k8sGeneration means number of offline services\n            k8sCount = totalRequests;\n        var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices, discoveryType);\n        GivenThereIsAFakeKubernetesProvider(endpoints, false, k8sGeneration, failPerThreads); // false means unstable, k8sGeneration services will be removed from the list\n\n        HighlyLoadOnKubeProviderAndRoundRobinBalancer(discoveryType, totalRequests, discoveryType == nameof(WatchKube) ? 0 : k8sGeneration, k8sCount);\n\n        ThenAllServicesCalledOptimisticAmountOfTimes(_roundRobinAnalyzer); // with unstable checkings\n        ThenServiceCountersShouldMatchLeasingCounters(_roundRobinAnalyzer, servicePorts, totalRequests);\n    }\n\n    [Theory]\n    [InlineData(nameof(Kube))]\n    [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304\n    [InlineData(nameof(WatchKube))]\n    [Trait(\"Feat\", \"2256\")]\n    public void ShouldReturnServicesFromK8s_AddKubernetesWithNullConfigureOptions(string discoveryType)\n    {\n        var servicePort = PortFinder.GetRandomPort();\n        var downstreamUrl = LoopbackLocalhostUrl(servicePort);\n        var downstream = new Uri(downstreamUrl);\n        var subsetV1 = GivenSubsetAddress(downstream);\n        var endpoints = GivenEndpoints(subsetV1);\n        var route = GivenRouteWithServiceName();\n        var configuration = GivenKubeConfiguration(route, discoveryType, \"txpc696iUhbVoudg164r93CxDTrKRVWG\");\n        string serviceName = ServiceName(), downstreamResponse = serviceName;\n        this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse))\n            .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning(AddKubernetesWithNullConfigureOptions))\n            .When(_ => GivenWatchReceivedEvent())\n            .When(_ => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe($\"1^:^{downstreamResponse}\"))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(1))\n            .And(x => x.ThenTheTokenIs(\"Bearer txpc696iUhbVoudg164r93CxDTrKRVWG\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2168\")]\n    [Trait(\"PR\", \"2174\")] // https://github.com/ThreeMammals/Ocelot/pull/2174\n    public void ShouldReturnServicesFromK8s_OneWatchRequestUpdatesServicesInfo()\n    {\n        (EndpointsV1 endpoints, string downstreamUrl) = GetServiceInstance();\n        (EndpointsV1 updatedEndpoints, string updateDownstreamUrl) = GetServiceInstance();\n        ResourceEventV1<EndpointsV1>[] events =\n        [\n            new() { EventType = ResourceEventType.Added, Resource = endpoints },\n            new() { EventType = ResourceEventType.Modified, Resource = updatedEndpoints }\n        ];\n        var route = GivenRouteWithServiceName();\n        var configuration = GivenKubeConfiguration(route, nameof(WatchKube));\n        \n        string serviceName = ServiceName(), downstreamResponse = serviceName;\n        var updatedDownstreamResponse = \"updated_content\" + serviceName;\n        this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse))\n            .Given(x => GivenServiceInstanceIsRunning(updateDownstreamUrl, updatedDownstreamResponse))\n            .And(x => x.GivenThereIsAFakeKubernetesProvider(events, serviceName))\n            .And(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => GivenOcelotIsRunning(WithKubernetes))\n            .When(_ => GivenWatchReceivedEvent())\n            .When(_ => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 10))\n            .Then(_ => ThenAllStatusCodesShouldBe(HttpStatusCode.OK))\n            .Then(_ => ThenAllResponseBodiesShouldBe(downstreamResponse))\n            .And(_ => ThenK8sShouldBeCalledExactly(1))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(10))\n            .When(_ => GivenWatchReceivedEvent())\n            .Given(_ => GivenIWaitAsync(100))\n            .When(_ => WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", 10))\n            .Then(_ => ThenAllStatusCodesShouldBe(HttpStatusCode.OK))\n            .Then(_ => ThenAllResponseBodiesShouldBe(updatedDownstreamResponse))\n            .And(_ => ThenK8sShouldBeCalledExactly(1))\n            .And(x => ThenAllServicesShouldHaveBeenCalledTimes(20))\n            .BDDfy();\n\n        (EndpointsV1 Endpoints, string DownstreamUrl) GetServiceInstance()\n        {\n            var servicePort = PortFinder.GetRandomPort();\n            var downstreamUrl = LoopbackLocalhostUrl(servicePort);\n            var downstream = new Uri(downstreamUrl);\n            var subset = GivenSubsetAddress(downstream);\n            var endpoints = GivenEndpoints(subset);\n            return (endpoints, downstreamUrl);\n        }\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")]\n    [Trait(\"PR\", \"2324\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    [InlineData(nameof(Kube))]\n    // [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304\n    [InlineData(nameof(WatchKube))]\n    public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes(string discoveryType)\n    {\n        static void ConfigureDynamicRouting(FileConfiguration configuration)\n        {\n            configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin));\n            configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp;\n            configuration.Routes = []; // dynamic routing\n            configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes\n        }\n        var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(\n            5, discoveryType, nameof(RoundRobin),\n            ConfigureDynamicRouting,\n            WithKubernetesAndFakeKubeServiceCreator);\n        GivenThereIsAFakeKubernetesProvider(endpoints);\n        if (discoveryType == nameof(WatchKube))\n            GivenWatchReceivedEvent();\n\n        var upstreamPath = $\"/{ServiceNamespace()}.{ServiceName()}/\";\n        WhenIGetUrlOnTheApiGatewayConcurrently(upstreamPath, 50);\n\n        if (discoveryType == nameof(PollKube))\n        {\n            //#if NET10_0_OR_GREATER\n            _k8sCounter.ShouldBeLessThan(50);\n            //#else\n            //            if (IsCiCd()) _k8sCounter.ShouldBeInRange(48, 52);\n            //            else _k8sCounter.ShouldBeGreaterThanOrEqualTo(50); // can be 50, 51 and sometimes 52\n            //#endif\n        }\n        else\n        {\n            _k8sCounter.ShouldBe(discoveryType == nameof(WatchKube) ? 1 : 50);\n        }\n\n        _k8sServiceGeneration.ShouldBe(0);\n        ThenAllStatusCodesShouldBe(HttpStatusCode.OK);\n        ThenAllServicesShouldHaveBeenCalledTimes(50);\n        ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion\n        ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion\n    }\n\n    private void AddKubernetesWithNullConfigureOptions(IServiceCollection services)\n        => services.AddOcelot().AddKubernetes(configureOptions: null);\n\n    private (EndpointsV1 Endpoints, int[] ServicePorts) GivenServiceDiscoveryAndLoadBalancing(\n        int totalServices,\n        string discoveryType = nameof(Kube),\n        string loadBalancerType = nameof(RoundRobinAnalyzer),\n        Action<FileConfiguration> configure = null,\n        Action<IServiceCollection> services = null,\n        [CallerMemberName] string serviceName = null)\n    {\n        serviceName ??= ServiceName();\n        var servicePorts = PortFinder.GetPorts(totalServices);\n        var downstreamUrls = servicePorts\n            .Select(port => LoopbackLocalhostUrl(port, Array.IndexOf(servicePorts, port))) // TODO Develop thread-safe version of the method\n            .ToArray(); // based on localhost aka loopback network interface\n        var downstreams = downstreamUrls.Select(url => new Uri(url))\n            .ToList();\n        var downstreamResponses = downstreams\n            .Select(ds => $\"{serviceName}:{ds.Host}:{ds.Port}\")\n            .ToArray();\n        var subset = new EndpointSubsetV1();\n        downstreams.ForEach(ds => GivenSubsetAddress(ds, subset));\n        var endpoints = GivenEndpoints(subset, serviceName); // totalServices service instances with different ports\n        var route = GivenRouteWithServiceName(serviceName, loadBalancerType); // !!!\n        var configuration = GivenKubeConfiguration(route, discoveryType.IfEmpty(nameof(Kube)));\n        configure?.Invoke(configuration);\n        GivenMultipleServiceInstancesAreRunning(downstreamUrls, downstreamResponses);\n        GivenThereIsAConfiguration(configuration);\n        int ocPort = GivenOcelotIsRunning(services ?? WithKubernetesAndRoundRobin);\n        return (endpoints, servicePorts);\n    }\n\n    private void HighlyLoadOnKubeProviderAndRoundRobinBalancer(string discoveryType, int totalRequests, int k8sGenerationNo, int? k8sCount = null)\n    {\n        if (discoveryType == nameof(WatchKube))\n        {\n            k8sCount = GivenWatchReceivedEvent(); // 1\n            k8sGenerationNo = 0;\n        }\n\n        // Act\n        WhenIGetUrlOnTheApiGatewayConcurrently(\"/\", totalRequests); // load by X parallel requests\n\n        // Assert\n        int count = k8sCount ?? totalRequests;\n        if (discoveryType == nameof(WatchKube))\n            _k8sCounter.ShouldBeLessThanOrEqualTo(count); // TODO This is something abnormal due to values 997-999, but actual value should be 1. Need to double check this.\n        if (discoveryType == nameof(PollKube))\n        {\n            //#if NET10_0_OR_GREATER\n            //_k8sCounter.ShouldBeLessThanOrEqualTo(count);\n            _k8sCounter.ShouldBeLessThan(count);\n            //#else\n            //            if (IsCiCd()) _k8sCounter.ShouldBeInRange(count - 1, count + 1);\n            //            else _k8sCounter.ShouldBeGreaterThanOrEqualTo(count); // can be 50, 51 and sometimes 52\n            //#endif\n        }\n        else\n            _k8sCounter.ShouldBeGreaterThanOrEqualTo(count); // integration endpoint called times\n\n        _k8sServiceGeneration.ShouldBe(k8sGenerationNo);\n#if NET10_0_OR_GREATER\n        if (discoveryType == nameof(PollKube))\n            _responses.Count(x => x.Value.StatusCode == HttpStatusCode.OK)\n                .ShouldBeGreaterThan(_responses.Count(x => x.Value.StatusCode != HttpStatusCode.OK));\n        else\n            ThenAllStatusCodesShouldBe(HttpStatusCode.OK);\n#else\n        ThenAllStatusCodesShouldBe(HttpStatusCode.OK);\n#endif\n\n#if NET10_0_OR_GREATER\n        if (discoveryType == nameof(PollKube))\n            _counters.Sum().ShouldBeLessThanOrEqualTo(totalRequests, CalledTimesMessage());\n        else\n            ThenAllServicesShouldHaveBeenCalledTimes(totalRequests);\n#else\n        ThenAllServicesShouldHaveBeenCalledTimes(totalRequests);\n#endif\n\n        _roundRobinAnalyzer.ShouldNotBeNull().Analyze();\n\n#if NET10_0_OR_GREATER\n        if (discoveryType == nameof(PollKube))\n            _roundRobinAnalyzer.Events.Count.ShouldBeInRange((int)(0.9 * totalRequests), totalRequests);\n        else\n            _roundRobinAnalyzer.Events.Count.ShouldBe(totalRequests);\n#else\n        _roundRobinAnalyzer.Events.Count.ShouldBe(totalRequests);\n#endif\n\n        _roundRobinAnalyzer.HasManyServiceGenerations(k8sGenerationNo).ShouldBeTrue();\n    }\n\n    private void ThenTheTokenIs(string token) => _receivedToken.ShouldBe(token);\n    private void ThenK8sShouldBeCalledExactly(int totalRequests) => _k8sCounter.ShouldBe(totalRequests);\n\n    private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = \"\")\n    {\n        var e = new EndpointsV1()\n        {\n            Kind = \"endpoint\",\n            ApiVersion = \"1.0\",\n            Metadata = new()\n            {\n                Name = serviceName,\n                Namespace = ServiceNamespace(),\n            },\n        };\n        e.Subsets.Add(subset);\n        return e;\n    }\n\n    private static EndpointSubsetV1 GivenSubsetAddress(Uri downstream, EndpointSubsetV1 subset = null)\n    {\n        subset ??= new();\n        subset.Addresses.Add(new()\n        {\n            Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), // 127.0.0.1\n            Hostname = downstream.Host,\n        });\n        subset.Ports.Add(new()\n        {\n            Name = downstream.Scheme,\n            Port = downstream.Port,\n        });\n        return subset;\n    }\n\n    private FileRoute GivenRouteWithServiceName([CallerMemberName] string serviceName = null,\n        string loadBalancerType = nameof(LeastConnection)) => new()\n        {\n            DownstreamPathTemplate = \"/\",\n            DownstreamScheme = null, // the scheme should not be defined in service discovery scenarios by default, only ServiceName\n            UpstreamPathTemplate = \"/\",\n            UpstreamHttpMethod = [HttpMethods.Get],\n            ServiceName = serviceName, // !!!\n            ServiceNamespace = ServiceNamespace(),\n            LoadBalancerOptions = new() { Type = loadBalancerType },\n        };\n\n    private FileConfiguration GivenKubeConfiguration(FileRoute route, string type, string token = null)\n    {\n        var u = new Uri(_kubernetesUrl);\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = u.Scheme,\n            Host = u.Host,\n            Port = u.Port,\n            Type = type,\n            PollingInterval = 5 * MaxKubernetesDelay, // 3ms is very fast polling, make sense for PollKube provider only\n            Namespace = ServiceNamespace(),\n            Token = token ?? \"Test\",\n        };\n        return configuration;\n    }\n\n    private const int MaxKubernetesDelay = 10; // ms\n    private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints,\n        [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests))\n        => GivenThereIsAFakeKubernetesProvider(endpoints, true, 0, 0, serviceName, ServiceNamespace());\n\n    private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints, bool isStable, int offlineServicesNo, int offlinePerThreads,\n        [CallerMemberName] string serviceName = null, string namespaces = null)\n    {\n        _k8sCounter = 0;\n        serviceName ??= ServiceName();\n        namespaces ??= ServiceNamespace();\n        handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context =>\n        {\n            await Task.Delay(Random.Shared.Next(1, MaxKubernetesDelay)); // emulate integration delay up to 10 milliseconds\n            if (context.Request.Path.Value == $\"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}\")\n            {\n                string json;\n                lock (K8sCounterLocker)\n                {\n                    _k8sCounter++;\n                    var subset = endpoints.Subsets[0];\n\n                    // Each offlinePerThreads-th request to integrated K8s endpoint should fail\n                    if (!isStable && _k8sCounter % offlinePerThreads == 0 && _k8sCounter >= offlinePerThreads)\n                    {\n                        while (offlineServicesNo-- > 0)\n                        {\n                            int index = subset.Addresses.Count - 1; // Random.Shared.Next(0, subset.Addresses.Count - 1);\n                            subset.Addresses.RemoveAt(index);\n                            subset.Ports.RemoveAt(index);\n                        }\n\n                        _k8sServiceGeneration++;\n                    }\n\n                    endpoints.Metadata.Generation = _k8sServiceGeneration;\n                    json = JsonConvert.SerializeObject(endpoints, KubeResourceClient.SerializerSettings);\n                }\n\n                if (context.Request.Headers.TryGetValue(\"Authorization\", out var values))\n                {\n                    _receivedToken = values.First();\n                }\n\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                await context.Response.WriteAsync(json);\n            }\n\n            await GivenHandleWatchRequest(context, \n                [new() { EventType = ResourceEventType.Added, Resource = endpoints }],\n                namespaces,\n                serviceName);\n        });\n    }\n    \n    private void GivenThereIsAFakeKubernetesProvider(ResourceEventV1<EndpointsV1>[] events,\n        [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests))\n    {\n        _k8sCounter = 0;\n        var namespaces = ServiceNamespace();\n        handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, (c) => GivenHandleWatchRequest(c, events, namespaces, serviceName));\n    }\n\n    private int GivenWatchReceivedEvent() => _k8sWatchResetEvent.Set() ? 1 : 0;\n    \n    private async Task GivenHandleWatchRequest(HttpContext context,\n        IEnumerable<ResourceEventV1<EndpointsV1>> events,\n        string namespaces,\n        string serviceName)\n    {\n        await Task.Delay(Random.Shared.Next(1, 10)); // emulate integration delay up to 10 milliseconds\n            \n        if (context.Request.Path.Value == $\"/api/v1/watch/namespaces/{namespaces}/endpoints/{serviceName}\")\n        {\n            _k8sCounter++;\n\n            if (context.Request.Headers.TryGetValue(\"Authorization\", out var values))\n            {\n                _receivedToken = values.First();\n            }\n\n            context.Response.StatusCode = 200;\n            context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n\n            foreach (var @event in events)\n            {\n                _k8sWatchResetEvent.WaitOne();\n                var json = JsonConvert.SerializeObject(@event, KubeResourceClient.SerializerSettings);\n                await using var sw = new StreamWriter(context.Response.Body);\n                await sw.WriteLineAsync(json);\n                await sw.FlushAsync();\n                _k8sWatchResetEvent.Reset();\n            }\n\n            // keeping open connection like kube api will slow down tests\n        }\n    }\n\n    private static ServiceDescriptor GetValidateScopesDescriptor()\n        => ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(\n            new DefaultServiceProviderFactory(new() { ValidateScopes = true }));\n    \n    private IOcelotBuilder AddKubernetes(IServiceCollection services) => services\n        .Replace(GetValidateScopesDescriptor())\n        .AddOcelot().AddKubernetes(_kubeClientOptionsConfigure);\n\n    private void WithKubernetes(IServiceCollection services) => AddKubernetes(services);\n    private void WithKubernetesAndFakeKubeServiceCreator(IServiceCollection services) => AddKubernetes(services)\n        .Services.RemoveAll<IKubeServiceCreator>().AddSingleton<IKubeServiceCreator, FakeKubeServiceCreator>();\n    private void WithKubernetesAndRoundRobin(IServiceCollection services) => AddKubernetes(services)\n        .AddCustomLoadBalancer<RoundRobinAnalyzer>(GetRoundRobinAnalyzer)\n        .Services.RemoveAll<IKubeServiceCreator>().AddSingleton<IKubeServiceCreator, FakeKubeServiceCreator>();\n\n    private int _k8sCounter, _k8sServiceGeneration;\n#if NET9_0_OR_GREATER\n    private static readonly Lock K8sCounterLocker = new();\n#else\n    private static readonly object K8sCounterLocker = new();\n#endif\n    private RoundRobinAnalyzer _roundRobinAnalyzer;\n    private AutoResetEvent _k8sWatchResetEvent = new(false);\n    private RoundRobinAnalyzer GetRoundRobinAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider)\n    {\n        lock (K8sCounterLocker)\n        {\n            return _roundRobinAnalyzer ??= new RoundRobinAnalyzerCreator().Create(route, provider)?.Data as RoundRobinAnalyzer; //??= new RoundRobinAnalyzer(provider.GetAsync, route.ServiceName);\n        }\n    }\n\n    protected override string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? nameof(KubernetesServiceDiscoveryTests);\n    protected override string ServiceNamespace() => nameof(KubernetesServiceDiscoveryTests);\n}\n\ninternal class FakeKubeServiceCreator : KubeServiceCreator\n{\n    public FakeKubeServiceCreator(IOcelotLoggerFactory factory) : base(factory) { }\n\n    protected override ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n    {\n        var ports = subset.Ports;\n        var index = subset.Addresses.IndexOf(address);\n        var portV1 = ports[index];\n        Logger.LogDebug(() => $\"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}].\");\n        return new ServiceHostAndPort(address.Ip, (int)portV1.Port, portV1.Name);\n    }\n\n    protected override IEnumerable<string> GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address)\n    {\n        var tags = base.GetServiceTags(configuration, endpoint, subset, address)\n            .ToList();\n        long gen = endpoint.Metadata.Generation ?? 0L;\n        tags.Add($\"{nameof(endpoint.Metadata.Generation)}:{gen}\");\n        return tags;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/PollKubeConcurrencyIntegrationTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n/// <summary>\n/// Concurrency integration tests for the PollKube service discovery provider.\n/// <para>\n/// Tests verify that the handler (Kubernetes API mock) is called the correct number of times based on polling intervals and concurrent request patterns.\n/// </para><para>\n/// The key metric is the handler counter which increments every time the Kubernetes API endpoint is called to fetch service endpoints.\n/// </para>\n/// </summary>\npublic class PollKubeConcurrencyIntegrationTests : Steps\n{\n    static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings;\n\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n\n    // Handler counter - tracks how many times the fake Kubernetes API was called\n    private int _kubeHandlerCallCount;\n#if NET9_0_OR_GREATER\n    private static readonly Lock _counterLock = new();\n#else\n    private static readonly object _counterLock = new();\n#endif\n\n    private const int PollingInterval = 100; // milliseconds\n    private const int FirstPollingWaitTime = 50; // milliseconds - less than polling interval\n    private const int SecondPollingWaitTime = 150; // milliseconds - slightly more than polling interval\n\n    public PollKubeConcurrencyIntegrationTests()\n    {\n        _factory = new();\n        _logger = new();\n        _factory.Setup(x => x.CreateLogger<PollKube>()).Returns(_logger.Object);\n        _factory.Setup(x => x.CreateLogger<Kube>()).Returns(_logger.Object);\n        _kubeHandlerCallCount = 0;\n    }\n\n    /// <summary>\n    /// Scenario 1: Single call after start - Handler called once (counter = 1)\n    /// \n    /// Arrange: Create PollKube provider\n    /// Act: Make single GetAsync() call\n    /// Assert: Handler counter should be 1 (cold start poll)\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario1\")]\n    [Trait(\"Feature\", \"PollingBehavior\")]\n    public async Task Scenario_1_SingleCallAfterStart_HandlerCalledOnce()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var expectedService = new Service(\"service-1\", new(\"localhost\", 8080), string.Empty, string.Empty, Array.Empty<string>());\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new[] { expectedService });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var services = await given.Provider.GetAsync();\n\n        // Assert\n        services.ShouldNotBeNull();\n        services.Count.ShouldBe(1);\n        _kubeHandlerCallCount.ShouldBe(1, \"Handler should be called exactly once for cold start\");\n    }\n\n    /// <summary>\n    /// Scenario 2: Three parallel calls within polling interval (before 2nd polling)\n    /// Handler called once (counter = 1)\n    /// \n    /// Multiple parallel calls should return the same queued service version without triggering additional polls.\n    /// All three calls occur before the next polling interval elapses.\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario2\")]\n    [Trait(\"Feature\", \"ParallelCalls\")]\n    public async Task Scenario_2_ThreeParallelCallsWithinFirstInterval_HandlerCalledOnce()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 1;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() => new Service[] { new($\"service-v{version}\", new(\"localhost\", 8080), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        // Make initial call to populate queue (cold start - this calls handler once)\n        var firstCall = await given.Provider.GetAsync();\n        firstCall.ShouldNotBeNull().ShouldNotBeEmpty();\n        firstCall[0].Name.ShouldBe(\"service-v1\");\n        _kubeHandlerCallCount.ShouldBe(1, \"First call should trigger cold start poll\");\n\n        // Make three parallel calls within the first polling interval (before 2nd polling)\n        await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken); // Wait less than polling interval\n        var parallelTasks = Enumerable.Range(0, 3)\n            .Select(_ => given.Provider.GetAsync())\n            .ToArray();\n        var results = await Task.WhenAll(parallelTasks);\n\n        // Assert\n        // All three parallel calls should return the same version\n        results.ShouldAllBe(r => r != null && r.Count == 1);\n        results.ShouldAllBe(r => r[0].Name == \"service-v1\");\n\n        // Handler should still be called only once - no additional polling occurred yet\n        _kubeHandlerCallCount.ShouldBe(1, \"Handler should not be called again - all parallel calls returned queued service\");\n    }\n\n    /// <summary>\n    /// Scenario 3: Multiple calls in 1st interval, 2nd polling returns new version\n    /// Handler called twice (counter = 2)\n    /// \n    /// First polling interval: multiple calls return version 1\n    /// Second polling interval: handler is called again, returns version 2\n    /// Then multiple calls return version 2\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario3\")]\n    [Trait(\"Feature\", \"PollingIntervals\")]\n    public async Task Scenario_3_FirstIntervalThenSecondPollingWithNewVersion_HandlerCalledTwice()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 1;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() => new Service[] { new($\"service-v{version}\", new(\"localhost\", 8000 + version), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act - First interval: make multiple calls\n        var firstCall = await given.Provider.GetAsync();\n        firstCall.ShouldNotBeNull();\n        firstCall[0].Name.ShouldBe(\"service-v1\");\n        firstCall[0].HostAndPort.DownstreamPort.ShouldBe(8001);\n        _kubeHandlerCallCount.ShouldBe(1);\n\n        // Multiple calls within first interval\n        await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        var parallelCalls1 = await Task.WhenAll(\n            Enumerable.Range(0, 3)\n                .Select(_ => given.Provider.GetAsync())\n                .ToArray());\n        parallelCalls1.ShouldAllBe(r => r[0].Name == \"service-v1\");\n        _kubeHandlerCallCount.ShouldBe(1, \"No additional polling yet\");\n\n        // Act - Wait for 2nd polling interval to occur\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); // Wait more than polling interval\n        version = 2; // Simulate version update\n\n        // Multiple calls in 2nd interval - should trigger second poll and return new version\n        var secondIntervalCalls = await Task.WhenAll(\n            Enumerable.Range(0, 3)\n                .Select(_ => given.Provider.GetAsync())\n                .ToArray());\n\n        // Assert\n        secondIntervalCalls.ShouldAllBe(r => r[0].Name == \"service-v2\");\n        secondIntervalCalls.ShouldAllBe(r => r[0].HostAndPort.DownstreamPort == 8002);\n\n        // Handler should have been called during the 2nd polling interval\n        _kubeHandlerCallCount.ShouldBe(2, \"2nd polling should have called handler\");\n    }\n\n    /// <summary>\n    /// Scenario 4: Multiple calls in each interval, polling returns new version each time\n    /// Handler called during each polling, counter increased by 1 for each poll\n    /// \n    /// This tests that polling happens at regular intervals and each poll increments the counter.\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario4\")]\n    [Trait(\"Feature\", \"RegularPolling\")]\n    public async Task Scenario_4_MultipleCallsEachIntervalNewVersionEachPolling_CounterIncreasedByOne()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 1;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() => new Service[] { new($\"service-v{version}\", new(\"localhost\", 9000 + version), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act & Assert - Interval 1\n        var call1 = await given.Provider.GetAsync();\n        call1[0].Name.ShouldBe(\"service-v1\");\n        _kubeHandlerCallCount.ShouldBe(1);\n\n        // Parallel calls in interval 1\n        await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        var interval1Calls = await Task.WhenAll(\n            Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray());\n        interval1Calls.ShouldAllBe(r => r[0].Name == \"service-v1\");\n        _kubeHandlerCallCount.ShouldBe(1);\n\n        // Act & Assert - Interval 2\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        version = 2;\n        var interval2Calls = await Task.WhenAll(\n            Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray());\n        interval2Calls.ShouldAllBe(r => r[0].Name == \"service-v2\");\n        _kubeHandlerCallCount.ShouldBe(2, \"Counter should be 2 after 2nd polling\");\n\n        // Act & Assert - Interval 3\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        version = 3;\n        var interval3Calls = await Task.WhenAll(\n            Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray());\n        interval3Calls.ShouldAllBe(r => r[0].Name == \"service-v3\");\n        _kubeHandlerCallCount.ShouldBe(3, \"Counter should be 3 after 3rd polling\");\n\n        // Act & Assert - Interval 4\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        version = 4;\n        var interval4Calls = await Task.WhenAll(\n            Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray());\n        interval4Calls.ShouldAllBe(r => r[0].Name == \"service-v4\");\n        _kubeHandlerCallCount.ShouldBe(4, \"Counter should be 4 after 4th polling\");\n    }\n\n    /// <summary>\n    /// Scenario 5: Heavy load (~1000 concurrent calls) during each polling interval\n    /// Handler called twice (counter = 2)\n    /// \n    /// Heavy load doesn't impact polling which happens at regular intervals.\n    /// Even with 1000 concurrent calls, the handler should be called exactly twice\n    /// (once for cold start, once for 2nd polling interval).\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario5\")]\n    [Trait(\"Feature\", \"HeavyLoad\")]\n    [Trait(\"Performance\", \"Stress\")]\n    public async Task Scenario_5_HeavyLoad1000CallsEachInterval_HandlerCalledTwice()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 1;\n        var callCount = 0;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() =>\n            {\n                Interlocked.Increment(ref callCount);\n                return new Service[] { new($\"service-v{version}\", new(\"localhost\", 7000 + version), string.Empty, string.Empty, Array.Empty<string>()) };\n            });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act - Cold start with heavy load\n        var coldStartHeavyLoad = await Task.WhenAll(\n            Enumerable.Range(0, 1000)\n                .Select(_ => given.Provider.GetAsync())\n                .ToArray());\n\n        // Assert - Cold start\n        coldStartHeavyLoad.ShouldAllBe(r => r != null && r.Count == 1);\n        // With heavy load, multiple threads might poll, but ideally only once\n        var coldStartCount = _kubeHandlerCallCount;\n        coldStartCount.ShouldBe(1, \"Cold start should call handler once despite 1000 concurrent calls\");\n\n        // Act - Wait for 2nd polling interval with heavy load\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        version = 2;\n\n        var secondIntervalHeavyLoad = await Task.WhenAll(\n            Enumerable.Range(0, 1000)\n                .Select(_ => given.Provider.GetAsync())\n                .ToArray());\n\n        // Assert - 2nd polling\n        secondIntervalHeavyLoad.ShouldAllBe(r => r != null && r.Count == 1);\n        secondIntervalHeavyLoad.ShouldAllBe(r => r[0].Name == \"service-v2\");\n\n        // Handler should be called exactly twice despite the heavy load\n        _kubeHandlerCallCount.ShouldBe(2,\n            \"Handler should be called exactly twice (cold start + 1st polling) despite 1000 concurrent calls per interval\");\n    }\n\n    /// <summary>\n    /// Extended Scenario 5b: Verify all responses are consistent during heavy load\n    /// \n    /// Even under heavy load with 1000+ concurrent calls, all responses should be identical\n    /// until the polling interval updates the services.\n    /// </summary>\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Concurrency\", \"Scenario5Extended\")]\n    [Trait(\"Feature\", \"ConsistencyUnderLoad\")]\n    public async Task Scenario_5b_HeavyLoadAllResponsesConsistent_NoPartialUpdates()\n    {\n        // Arrange\n        _kubeHandlerCallCount = 0;\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 1;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() => new Service[] { new($\"service-v{version}\", new(\"localhost\", 6000 + version), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpointsWithVersion(1);\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act - Cold start\n        var coldStartResponses = await Task.WhenAll(\n            Enumerable.Range(0, 500)\n                .Select(_ => given.Provider.GetAsync())\n                .ToArray());\n\n        // Assert - All responses should be for version 1\n        var v1Count = coldStartResponses.Count(r => r[0].Name == \"service-v1\");\n        v1Count.ShouldBe(500, \"All cold start responses should be version 1\");\n        _kubeHandlerCallCount.ShouldBe(1);\n\n        // Act - Wait and trigger 2nd polling\n        await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken);\n        version = 2;\n\n        // Heavy load that might span polling interval boundary\n        var heavyLoadResponses = await Task.WhenAll(\n            Enumerable.Range(0, 1500)\n                .Select(async (i) =>\n                {\n                    if (i % 500 == 0)\n                        await Task.Delay(10); // Small delay to allow polling to happen\n                    return await given.Provider.GetAsync();\n                })\n                .ToArray());\n\n        // Assert - All responses should be version 1 (queued before 2nd polling)\n        // or version 2 (after 2nd polling), not a mix\n        var v2Count = heavyLoadResponses.Count(r => r[0].Name == \"service-v2\");\n        var allV1 = heavyLoadResponses.All(r => r[0].Name == \"service-v1\");\n        var allV2 = heavyLoadResponses.All(r => r[0].Name == \"service-v2\");\n\n        (allV1 || allV2).ShouldBeTrue(\"All responses should be consistent - either all v1 or all v2\");\n\n        // Handler should be called twice total\n        _kubeHandlerCallCount.ShouldBe(2, \"Should have exactly 2 handler calls\");\n    }\n\n    #region Helper Methods\n\n    private (IKubeApiClient Client, KubeClientOptions ClientOptions, PollKube Provider, KubeRegistryConfiguration ProviderOptions)\n        GivenClientAndPollKubeProvider(out Mock<IKubeServiceBuilder> serviceBuilder, int pollingInterval = PollingInterval, [CallerMemberName] string serviceName = null)\n    {\n        serviceName ??= serviceName;\n        var kubePort = PortFinder.GetRandomPort();\n        var options = new KubeClientOptions\n        {\n            AccessToken = serviceName, // \"txpc696iUhbVoudg164r93CxDTrKRVWG\",\n            AllowInsecure = true,\n            ApiEndPoint = new(DownstreamUrl(kubePort)),\n            AuthStrategy = KubeAuthStrategy.BearerToken,\n        };\n\n        IKubeApiClient client = KubeApiClient.Create(options);\n\n        var config = new KubeRegistryConfiguration\n        {\n            KeyOfServiceInK8s = serviceName,\n            KubeNamespace = nameof(PollKubeConcurrencyIntegrationTests),\n        };\n\n        serviceBuilder = new();\n        var kubeProvider = new Kube(config, _factory.Object, client, serviceBuilder.Object);\n        var provider = new PollKube(pollingInterval, _factory.Object, kubeProvider);\n\n        return (client, options, provider, config);\n    }\n\n    protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(\n        string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy<string> receivedToken)\n    {\n        var token = string.Empty;\n        receivedToken = new(() => token);\n        handler.GivenThereIsAServiceRunningOn(url, ProcessKubernetesRequest);\n\n        Task ProcessKubernetesRequest(HttpContext context)\n        {\n            if (context.Request.Path.Value == $\"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}\")\n            {\n                // Increment handler call counter - this is the key metric being tested\n                lock (_counterLock)\n                {\n                    _kubeHandlerCallCount++;\n                }\n\n                if (context.Request.Headers.TryGetValue(\"Authorization\", out var values))\n                {\n                    token = values.First();\n                }\n\n                var responseBody = JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings);\n                context.Response.StatusCode = (int)HttpStatusCode.OK;\n                context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n                return context.Response.WriteAsync(responseBody);\n            }\n\n            return Task.CompletedTask;\n        }\n    }\n\n    private static EndpointsV1 GivenEndpointsWithVersion(int version)\n    {\n        var endpoints = new EndpointsV1\n        {\n            Metadata = new ObjectMetaV1\n            {\n                Name = \"test-endpoints\",\n                Namespace = nameof(PollKubeConcurrencyIntegrationTests),\n                Generation = version,\n            },\n        };\n        var subset = new EndpointSubsetV1();\n        endpoints.Subsets.Add(subset);\n        subset.Addresses.Add(new()\n        {\n            Ip = \"127.0.0.1\",\n            TargetRef = new ObjectReferenceV1 { Name = \"pod-1\" },\n        });\n        subset.Ports.Add(new()\n        {\n            Name = \"http\",\n            Port = 8080,\n        });\n        return endpoints;\n    }\n    #endregion\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/PollKubeIntegrationTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing Microsoft.AspNetCore.Http;\nusing Newtonsoft.Json;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\n/// <summary>\n/// Integration tests for the <see cref=\"PollKube\"/> service discovery provider.\n/// <see cref=\"PollKube\"/> polls the Kubernetes API at specified intervals to discover services.\n/// </summary>\n[Trait(\"Milestone\", \".NET 10\")] // https://github.com/ThreeMammals/Ocelot/milestone/13\n[Trait(\"Release\", \"25.0.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0\npublic class PollKubeIntegrationTests : Steps\n{\n    static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings;\n\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private const int PollingInterval = 100; // milliseconds\n\n    public PollKubeIntegrationTests()\n    {\n        _factory = new();\n        _logger = new();\n        _factory.Setup(x => x.CreateLogger<PollKube>()).Returns(_logger.Object);\n        _factory.Setup(x => x.CreateLogger<Kube>()).Returns(_logger.Object);\n    }\n\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Feature\", \"Polling\")]\n    public async Task Should_return_service_from_k8s_on_first_call()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var expectedService = new Service(\n            nameof(Should_return_service_from_k8s_on_first_call),\n            new(\"localhost\", 8080),\n            string.Empty,\n            string.Empty,\n            Array.Empty<string>());\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new[] { expectedService });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> receivedToken);\n\n        // Act - First call should perform initial poll\n        var services = await given.Provider.GetAsync();\n\n        // Assert\n        services.ShouldNotBeNull();\n        services.Count.ShouldBe(1);\n        services[0].HostAndPort.DownstreamHost.ShouldBe(\"localhost\");\n        services[0].HostAndPort.DownstreamPort.ShouldBe(8080);\n        receivedToken.Value.ShouldContain(\"Bearer\");\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"Polling\")]\n    [Trait(\"Concurrency\", \"Multiple\")]\n    public async Task Should_return_queued_service_on_concurrent_calls()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var expectedService = new Service(\n            nameof(Should_return_queued_service_on_concurrent_calls),\n            new(\"localhost\", 9090),\n            string.Empty,\n            string.Empty,\n            Array.Empty<string>());\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new[] { expectedService });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> receivedToken);\n\n        // Act - First call to populate queue\n        var firstCall = await given.Provider.GetAsync();\n        firstCall.ShouldNotBeNull();\n        firstCall.Count.ShouldBe(1);\n\n        // Act - Multiple concurrent calls should return queued service\n        var tasks = Enumerable.Range(0, 5)\n            .Select(_ => given.Provider.GetAsync())\n            .ToArray();\n        var results = await Task.WhenAll(tasks);\n\n        // Assert\n        results.ShouldAllBe(r => r != null && r.Count == 1);\n        results.ShouldAllBe(r => r[0].HostAndPort.DownstreamPort == 9090);\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"Polling\")]\n    [Trait(\"Timing\", \"Interval\")]\n    public async Task Should_poll_at_specified_intervals()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder, pollingInterval: 50);\n        var callCount = 0;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() =>\n            {\n                Interlocked.Increment(ref callCount);\n                return new Service[] { new(\"service\", new(\"localhost\", callCount * 1000), string.Empty, string.Empty, Array.Empty<string>()) };\n            });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var firstServices = await given.Provider.GetAsync();\n        firstServices.ShouldNotBeNull();\n\n        // Wait for polling interval to elapse and check if new service version is queued\n        await Task.Delay(PollingInterval, Xunit.TestContext.Current.CancellationToken); // Wait for at least one or two polling cycles\n\n        var secondServices = await given.Provider.GetAsync();\n\n        // Assert - Services should not be empty and polling should have occurred\n        secondServices.ShouldNotBeNull();\n        callCount.ShouldBeGreaterThanOrEqualTo(1);\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"QueueManagement\")]\n    [Trait(\"Behavior\", \"OldVersionRemoval\")]\n    public async Task Should_remove_outdated_versions_and_keep_latest()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var version = 0;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() =>\n            {\n                version++;\n                return new Service[] { new($\"service-v{version}\", new(\"localhost\", version), string.Empty, string.Empty, Array.Empty<string>()) };\n            });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var firstCall = await given.Provider.GetAsync();\n        firstCall.ShouldNotBeNull();\n\n        // Wait for multiple polling cycles\n        await Task.Delay(300, Xunit.TestContext.Current.CancellationToken);\n\n        var lastCall = await given.Provider.GetAsync();\n\n        // Assert - Should get the latest version with the highest port number\n        lastCall.ShouldNotBeNull();\n        lastCall.Count.ShouldBe(1);\n        lastCall[0].HostAndPort.DownstreamPort.ShouldBeGreaterThan(1);\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"ErrorHandling\")]\n    public async Task Should_return_empty_when_provider_disposed()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new Service[] { new(\"test\", new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var services = await given.Provider.GetAsync();\n        services.ShouldNotBeNull();\n\n        // Dispose the provider\n        given.Provider.Dispose();\n        await Task.Delay(200, Xunit.TestContext.Current.CancellationToken);\n\n        // Try to get services after disposal - should return empty\n        var servicesAfterDisposal = await given.Provider.GetAsync();\n\n        // Assert\n        servicesAfterDisposal.ShouldNotBeNull();\n        servicesAfterDisposal.Count.ShouldBe(0);\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"ErrorHandling\")]\n    [Trait(\"Scenario\", \"KubeAPIError\")]\n    public async Task Should_handle_k8s_api_error_gracefully()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new Service[] { new(\"test\", new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.InternalServerError,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var services = await given.Provider.GetAsync();\n\n        // Assert - Should return empty list on error\n        services.ShouldNotBeNull();\n        // First call may return empty due to API error\n    }\n\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Feature\", \"ColdStart\")]\n    public async Task Should_perform_initial_poll_on_first_call_when_queue_is_empty()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var initialService = new Service(\n            \"initial-service\",\n            new(\"localhost\", 5000),\n            string.Empty,\n            string.Empty,\n            Array.Empty<string>());\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new[] { initialService });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act - First call on newly created provider with empty queue\n        var services = await given.Provider.GetAsync();\n\n        // Assert\n        services.ShouldNotBeNull();\n        services.Count.ShouldBe(1);\n        services[0].Name.ShouldBe(\"initial-service\");\n        services[0].HostAndPort.DownstreamPort.ShouldBe(5000);\n    }\n\n    [Fact]\n    [Trait(\"Feature\", \"QueueManagement\")]\n    public async Task Should_not_enqueue_services_when_already_polling()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        var pollCount = 0;\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(() =>\n            {\n                Interlocked.Increment(ref pollCount);\n                return new Service[] { new($\"service-poll-{pollCount}\", new(\"localhost\", 8000), string.Empty, string.Empty, Array.Empty<string>()) };\n            });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var firstCall = await given.Provider.GetAsync();\n        firstCall.ShouldNotBeNull();\n\n        // Assert\n        pollCount.ShouldBeGreaterThanOrEqualTo(1);\n    }\n\n    [Fact(Skip = \"Under development\")]\n    [Trait(\"Feature\", \"Threading\")]\n    public async Task Should_safely_handle_disposal_during_polling()\n    {\n        // Arrange\n        var given = GivenClientAndPollKubeProvider(out var serviceBuilder);\n        serviceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns(new Service[] { new(\"test\", new(\"localhost\", 80), string.Empty, string.Empty, Array.Empty<string>()) });\n\n        var endpoints = GivenEndpoints();\n        GivenThereIsAFakeKubeServiceDiscoveryProvider(\n            given.ClientOptions.ApiEndPoint.ToString(),\n            given.ProviderOptions.KubeNamespace,\n            given.ProviderOptions.KeyOfServiceInK8s,\n            statusCode: HttpStatusCode.OK,\n            endpoints,\n            out Lazy<string> _);\n\n        // Act\n        var getServiceTask = given.Provider.GetAsync();\n        await Task.Delay(10, Xunit.TestContext.Current.CancellationToken); // Let the polling start\n        given.Provider.Dispose();\n\n        // Assert - Provider should be disposed\n        var servicesAfterDisposal = await getServiceTask;\n        servicesAfterDisposal.ShouldNotBeNull();\n        servicesAfterDisposal.Count.ShouldBe(0);\n    }\n\n    #region Helper Methods\n\n    private (IKubeApiClient Client, KubeClientOptions ClientOptions, PollKube Provider, KubeRegistryConfiguration ProviderOptions)\n        GivenClientAndPollKubeProvider(out Mock<IKubeServiceBuilder> serviceBuilder, int pollingInterval = PollingInterval, [CallerMemberName] string serviceName = null)\n    {\n        serviceName ??= serviceName;\n        var kubePort = PortFinder.GetRandomPort();\n        var options = new KubeClientOptions\n        {\n            AccessToken = serviceName, // \"txpc696iUhbVoudg164r93CxDTrKRVWG\",\n            AllowInsecure = true,\n            ApiEndPoint = new(DownstreamUrl(kubePort)),\n            AuthStrategy = KubeAuthStrategy.BearerToken,\n        };\n\n        IKubeApiClient client = KubeApiClient.Create(options);\n\n        var config = new KubeRegistryConfiguration\n        {\n            KeyOfServiceInK8s = serviceName,\n            KubeNamespace = nameof(PollKubeIntegrationTests),\n        };\n\n        serviceBuilder = new();\n\n        // Create the inner Kube provider\n        var kubeProvider = new Kube(config, _factory.Object, client, serviceBuilder.Object);\n\n        // Wrap with PollKube\n        var provider = new PollKube(pollingInterval, _factory.Object, kubeProvider);\n\n        return (client, options, provider, config);\n    }\n\n    protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(\n        string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy<string> receivedToken)\n        => GivenThereIsAFakeKubeServiceDiscoveryProvider(url, namespaces, serviceName, HttpStatusCode.OK, endpointEntries, out receivedToken);\n\n    protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(\n        string url, string namespaces, string serviceName,\n        HttpStatusCode statusCode, EndpointsV1 endpointEntries, out Lazy<string> receivedToken)\n    {\n        var token = string.Empty;\n        receivedToken = new(() => token);\n        handler.GivenThereIsAServiceRunningOn(url, async context =>\n        {\n            if (context.Request.Path.Value != $\"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}\")\n                return;\n\n            var responseBody = string.Empty;\n            if (context.Request.Headers.TryGetValue(\"Authorization\", out var values))\n            {\n                token = values.First();\n            }\n\n                responseBody = statusCode == HttpStatusCode.OK\n                    ? JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings)\n                    : JsonConvert.SerializeObject(new StatusV1\n                        {\n                            Message = GetKubeApiErrorMessage(serviceName, namespaces, statusCode),\n                            Reason = statusCode.ToString(),\n                            Code = (int)statusCode,\n                            Status = StatusV1.FailureStatus,\n                        }, JsonSerializerSettings);\n\n            context.Response.StatusCode = (int)statusCode;\n            context.Response.Headers.Append(\"Content-Type\", \"application/json\");\n            await context.Response.WriteAsync(responseBody);\n        });\n    }\n\n    private static EndpointsV1 GivenEndpoints()\n    {\n        var endpoints = new EndpointsV1\n        {\n            Metadata = new ObjectMetaV1 { Name = \"test-endpoints\", Namespace = nameof(PollKubeIntegrationTests) },\n        };\n        var subset = new EndpointSubsetV1();\n        endpoints.Subsets.Add(subset);\n        subset.Addresses.Add(new()\n        {\n            Ip = \"127.0.0.1\", TargetRef = new ObjectReferenceV1 { Name = \"pod-1\" },\n        });\n        subset.Ports.Add(new()\n        {\n            Name = \"http\", Port = 8080,\n        });\n        return endpoints;\n    }\n\n    private static string GetKubeApiErrorMessage(string serviceName, string kubeNamespace, HttpStatusCode responseStatusCode)\n    {\n        return responseStatusCode switch\n        {\n            HttpStatusCode.NotFound => $\"endpoints \\\"{serviceName}\\\" not found\",\n            HttpStatusCode.Forbidden => $\"endpoints \\\"{serviceName}\\\" is forbidden: User \\\"system:serviceaccount:default:default\\\" cannot get resource \\\"endpoints\\\" in API group \\\"\\\" in the namespace \\\"{kubeNamespace}\\\"\",\n            HttpStatusCode.BadRequest => $\"Bad Request: endpoints \\\"{serviceName}\\\" in namespace \\\"{kubeNamespace}\\\" is invalid\",\n            _ => $\"Failed to retrieve endpoints \\\"{serviceName}\\\" in namespace \\\"{kubeNamespace}\\\"\",\n        };\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.AcceptanceTests.ServiceDiscovery;\n\npublic sealed class ServiceFabricTests : Steps\n{\n    private string _downstreamPath;\n\n    public ServiceFabricTests()\n    {\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"570\")]\n    [Trait(\"Bug\", \"555\")]\n    public void Should_fix_issue_555()\n    {\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n                {\n                    new()\n                    {\n                        DownstreamPathTemplate = \"/{everything}\",\n                        DownstreamScheme = \"http\",\n                        UpstreamPathTemplate = \"/{everything}\",\n                        UpstreamHttpMethod = [\"Get\"],\n                        ServiceName = \"OcelotServiceApplication/OcelotApplicationService\",\n                    },\n                },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Host = \"localhost\",\n                    Port = port,\n                    Type = \"ServiceFabric\",\n                },\n            },\n        };\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/OcelotServiceApplication/OcelotApplicationService/a\", 200, \"Hello from Laura\", \"b=c\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/a?b=c\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_support_service_fabric_naming_and_dns_service_stateless_and_guest()\n    {\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n                {\n                    new()\n                    {\n                        DownstreamPathTemplate = \"/api/values\",\n                        DownstreamScheme = \"http\",\n                        UpstreamPathTemplate = \"/EquipmentInterfaces\",\n                        UpstreamHttpMethod = [\"Get\"],\n                        ServiceName = \"OcelotServiceApplication/OcelotApplicationService\",\n                    },\n                },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Host = \"localhost\",\n                    Port = port,\n                    Type = \"ServiceFabric\",\n                },\n            },\n        };\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/OcelotServiceApplication/OcelotApplicationService/api/values\", 200, \"Hello from Laura\", \"test=best\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/EquipmentInterfaces?test=best\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_support_service_fabric_naming_and_dns_service_statefull_and_actors()\n    {\n        var port = PortFinder.GetRandomPort();\n        var configuration = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    DownstreamPathTemplate = \"/api/values\",\n                    DownstreamScheme = \"http\",\n                    UpstreamPathTemplate = \"/EquipmentInterfaces\",\n                    UpstreamHttpMethod = [\"Get\"],\n                    ServiceName = \"OcelotServiceApplication/OcelotApplicationService\",\n                },\n            },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Host = \"localhost\",\n                    Port = port,\n                    Type = \"ServiceFabric\",\n                },\n            },\n        };\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/OcelotServiceApplication/OcelotApplicationService/api/values\", 200, \"Hello from Laura\", \"PartitionKind=test&PartitionKey=1\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/EquipmentInterfaces?PartitionKind=test&PartitionKey=1\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\n            .BDDfy();\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"722\")]\n    [Trait(\"Feat\", \"721\")]\n    [InlineData(\"/api/{version}/values\", \"/values\", \"Service_{version}/Api\", \"/Service_1.0/Api/values\", \"/api/1.0/values?test=best\", \"test=best\")]\n    [InlineData(\"/api/{version}/{all}\", \"/{all}\", \"Service_{version}/Api\", \"/Service_1.0/Api/products\", \"/api/1.0/products?test=the-best-from-Aly\", \"test=the-best-from-Aly\")]\n    public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query)\n    {\n        var port = PortFinder.GetRandomPort();\n\n        var configuration = new FileConfiguration\n        {\n            Routes = new()\n                {\n                    new()\n                    {\n                        DownstreamPathTemplate = downstream,\n                        DownstreamScheme = Uri.UriSchemeHttp,\n                        UpstreamPathTemplate = upstream,\n                        UpstreamHttpMethod = [HttpMethods.Get],\n                        ServiceName = serviceName,\n                    },\n                },\n            GlobalConfiguration = new FileGlobalConfiguration\n            {\n                ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n                {\n                    Host = \"localhost\",\n                    Port = port,\n                    Type = \"ServiceFabric\",\n                },\n            },\n        };\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamUrl, 200, \"Hello from Felix Boers\", query))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(url))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Felix Boers\"))\n            .BDDfy();\n    }\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode, string responseBody, string expectedQueryString)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>\n        {\n            _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\n\n            if (_downstreamPath != basePath)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.NotFound;\n                await context.Response.WriteAsync(\"downstream path didnt match base path\");\n            }\n            else\n            {\n                if (context.Request.QueryString.Value.Contains(expectedQueryString))\n                {\n                    context.Response.StatusCode = statusCode;\n                    await context.Response.WriteAsync(responseBody);\n                }\n                else\n                {\n                    context.Response.StatusCode = (int)HttpStatusCode.NotFound;\n                    await context.Response.WriteAsync(\"downstream path didnt match base path\");\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/SslTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic sealed class SslTests : Steps\r\n{\r\n    public SslTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_dangerous_accept_any_server_certificate_validator()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenSslRoute(port, true);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Laura\"))\r\n            .BDDfy();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_dangerous_accept_any_server_certificate_validator()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenSslRoute(port, false);\r\n        var configuration = GivenConfiguration(route);\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => GivenOcelotIsRunning())\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway))\r\n            .BDDfy();\r\n    }\r\n\r\n    private FileRoute GivenSslRoute(int port, bool validatorEnabled)\r\n    {\r\n        var route = GivenDefaultRoute(port);\r\n        route.DownstreamScheme = Uri.UriSchemeHttps;\r\n        route.DangerousAcceptAnyServerCertificateValidator = validatorEnabled;\r\n        return route;\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAHttpsServiceRunningOn(DownstreamUrl(port), basePath, \"mycert2.pfx\", \"password\", port, async context =>\r\n        {\r\n            var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\r\n            bool oK = downstreamPath == basePath;\r\n            context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound;\r\n            await context.Response.WriteAsync(oK ? responseBody : \"downstream path didn't match base path\");\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/StartupTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.AcceptanceTests;\r\n\r\npublic class StartupTests : Steps\r\n{\r\n    public StartupTests()\r\n    {\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = GivenDefaultRoute(port);\r\n        var configuration = GivenConfiguration(route);\r\n        var fakeRepo = new FakeFileConfigurationRepository();\r\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpStatusCode.OK, \"Hello from Laura\"))\r\n            .And(x => GivenThereIsAConfiguration(configuration))\r\n            .And(x => x.GivenOcelotIsRunningWithBlowingUpDiskRepo(fakeRepo))\r\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\r\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\r\n            .BDDfy();\r\n    }\r\n\r\n    private void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake)\r\n    {\r\n        void WithFakeRepo(IServiceCollection s) => s.AddSingleton(fake).AddOcelot();\r\n        GivenOcelotIsRunning(WithFakeRepo);\r\n    }\r\n\r\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody)\r\n    {\r\n        handler.GivenThereIsAServiceRunningOn(port, basePath, context =>\r\n        {\r\n            var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;\r\n            bool oK = downstreamPath == basePath;\r\n            context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound;\r\n            return context.Response.WriteAsync(oK ? responseBody : \"downstream path didn't match base path\");\r\n        });\r\n    }\r\n\r\n    private class FakeFileConfigurationRepository : IFileConfigurationRepository\r\n    {\r\n        public Task<Response<FileConfiguration>> Get() => throw new NotImplementedException();\r\n        public Task<Response> Set(FileConfiguration fileConfiguration) => throw new NotImplementedException();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Steps.cs",
    "content": "﻿using Ocelot.AcceptanceTests.Properties;\r\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.AcceptanceTests;\n\npublic class Steps : AcceptanceSteps\r\n{\n    public Steps() : base()\r\n    {\n        BddfyConfig.Configure();\r\n    }\r\n    public static bool IsCiCd() => IsRunningInGitHubActions();\r\n    public static bool IsRunningInGitHubActions()\r\n        => Environment.GetEnvironmentVariable(\"GITHUB_ACTIONS\") == \"true\";\r\n\n    public void GivenOcelotIsRunningWithDelegatingHandler<THandler>(bool global = false)\r\n        where THandler : DelegatingHandler\r\n        => GivenOcelotIsRunning(s => s.AddOcelot().AddDelegatingHandler<THandler>(global));\n\n    public Task<int> GivenOcelotIsRunningAsync(OcelotPipelineConfiguration pipelineConfig)\r\n        => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot,\r\n            async a => await a.UseOcelot(pipelineConfig));\n\r\n    #region TODO: Move to Ocelot.Testing package\r\n    #endregion\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Transformations/ClaimsToDownstreamPathTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.VisualStudio.TestPlatform.ObjectModel;\nusing Ocelot.AcceptanceTests.Authorization;\nusing Ocelot.Testing.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Transformations;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-downstream-path\">Claims to Downstream Path</see>.\n/// </summary>\n[Trait(\"Feat\", \"968\")] // https://github.com/ThreeMammals/Ocelot/pull/968\n[Trait(\"Release\", \"13.8.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0\npublic sealed class ClaimsToDownstreamPathTests : AuthorizationSteps\n{\n    [Fact]\n    public void Should_return_200_OK_and_change_downstream_path()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"openid\", \"offline_access\", \"api\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        route.DownstreamPathTemplate = \"/users/{userId}\";\n        route.UpstreamPathTemplate = \"/users/{userId}\";\n        route.ChangeDownstreamPathTemplate = new()\n        {\n            { \"userId\", $\"Claims[{OcelotClaims.OcSub}] > value[1] > |\" },\n        };\n        var configuration = GivenConfiguration(route);\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Victor\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveAToken(testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/users\"))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Victor\"))\n            .And(x => ThenTheDownstreamPathIs(\"/users/1234567890\"))\n            .BDDfy();\n    }\n\n    private const string UserId = \"1234567890\";\n    protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e)\n    {\n        e.Request.UserId += \"|\" + UserId; // -> sub claim -> oc-sub claim\n    }\n\n    private string _downstreamFinalPath;\n    private void ThenTheDownstreamPathIs(string path)\n    {\n        _downstreamFinalPath.ShouldBe(path);\n    }\n    protected override Task MapStatus(HttpContext context)\n    {\n        _downstreamFinalPath = context.Request.Path.Value;\n        return base.MapStatus(context);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Transformations/ClaimsToHeadersForwardingTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.AcceptanceTests.Authorization;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.Testing.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Transformations;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-headers\">Claims to Headers</see>.\n/// </summary>\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic sealed class ClaimsToHeadersForwardingTests : AuthorizationSteps\n{\n    [Fact]\n    public void Should_return_200_OK_and_forward_claim_as_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"openid\", \"offline_access\", \"api\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        route.AddHeadersToRequest = new()\n        {\n            { \"CustomerId\", \"Claims[CustomerId] > value\" },\n            { \"LocationId\", \"Claims[LocationId] > value\" },\n            { \"UserType\", $\"Claims[{OcelotClaims.OcSub}] > value[0] > |\" },\n            { \"UserId\", $\"Claims[{OcelotClaims.OcSub}] > value[1] > |\" },\n        };\n        var configuration = GivenConfiguration(route);\n        var claims = new Dictionary<string, string>()\n        {\n            { \"CustomerId\", \"111\" },\n            { \"LocationId\", \"222\" },\n        };\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Tom\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"Hello from Tom\"))\n            .And(x => ThenTheResponseHeaderIs(\"RequestHeaders\", \"CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_header UserId:1234567890\"))\n            .BDDfy();\n    }\n\n    private const string UserId = \"1234567890\";\n    protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e)\n    {\n        e.Request.UserId += \"|\" + UserId; // -> sub claim -> oc-sub claim\n    }\n\n    protected override Task MapStatus(HttpContext context)\n    {\n        var customerId = context.Request.Headers.GetCommaSeparatedValues(\"CustomerId\").Csv();\n        var locationId = context.Request.Headers.GetCommaSeparatedValues(\"LocationId\").Csv();\n        var userType = context.Request.Headers.GetCommaSeparatedValues(\"UserType\").Csv();\n        var userId = context.Request.Headers.GetCommaSeparatedValues(\"UserId\").Csv();\n        var responseBody = $\"CustomerId:{customerId} LocationId:{locationId} UserType:{userType} UserId:{userId}\";\n        context.Response.Headers.Append(\"RequestHeaders\", responseBody);\n        return base.MapStatus(context);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Transformations/ClaimsToQueryStringForwardingTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.AcceptanceTests.Authorization;\nusing Ocelot.Configuration.File;\nusing Ocelot.Testing.Authentication;\n\nnamespace Ocelot.AcceptanceTests.Transformations;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-query-string-parameters\">Claims to Query String Parameters</see>.\n/// </summary>\n[Trait(\"Commit\", \"f7f4a39\")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic sealed class ClaimsToQueryStringForwardingTests : AuthorizationSteps\n{\n    private static Dictionary<string, string> GivenAddQueriesToRequest(FileRoute route)\n    {\n        route.AddQueriesToRequest = new()\n        {\n            { \"CustomerId\", \"Claims[CustomerId] > value\" },\n            { \"LocationId\", \"Claims[LocationId] > value\" },\n            { \"UserType\", $\"Claims[{OcelotClaims.OcSub}] > value[0] > |\" },\n            { \"UserId\", $\"Claims[{OcelotClaims.OcSub}] > value[1] > |\" },\n        };\n        var claims = new Dictionary<string, string>()\n        {\n            { \"CustomerId\", \"111\" },\n            { \"LocationId\", \"222\" },\n        };\n        return claims;\n    }\n\n    [Fact]\n    public void Should_return_200_OK_and_forward_claim_as_query_string()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"openid\", \"offline_access\", \"api\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        var configuration = GivenConfiguration(route);\n        var claims = GivenAddQueriesToRequest(route);\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Tom\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_query_string UserId:1234567890\"))\n            .And(x => ThenTheQueryStringIs(\"?CustomerId=111&LocationId=222&UserId=1234567890&UserType=Should_return_200_OK_and_forward_claim_as_query_string\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string()\n    {\n        var port = PortFinder.GetRandomPort();\n        string[] allowedScopes = [\"openid\", \"offline_access\", \"api\"];\n        var route = GivenAuthRoute(port, scopes: allowedScopes);\n        var configuration = GivenConfiguration(route);\n        var claims = GivenAddQueriesToRequest(route);\n        var testName = TestName();\n        this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication))\n            .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, \"Hello from Tom\"))\n            .And(x => GivenIUpdateSubClaim())\n            .And(x => GivenIHaveATokenWithClaims(claims, testName))\n            .And(x => GivenIHaveAddedATokenToMyRequest())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/?test=1&test=2\"))\n            .Then(x => ThenTheStatusCodeShouldBeOK())\n            .And(x => ThenTheResponseBodyShouldBe(\"CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string UserId:1234567890\"))\n            .And(x => ThenTheQueryStringIs(\"?test=1&test=2&CustomerId=111&LocationId=222&UserId=1234567890&UserType=Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string\"))\n            .BDDfy();\n    }\n\n    private string _downstreamQueryString;\n    private void ThenTheQueryStringIs(string queryString)\n    {\n        _downstreamQueryString.ShouldBe(queryString);\n    }\n\n    private const string UserId = \"1234567890\";\n    protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e)\n    {\n        e.Request.UserId += \"|\" + UserId; // -> sub claim -> oc-sub claim\n    }\n\n    protected override Task MapStatus(HttpContext context)\n    {\n        _downstreamQueryString = context.Request.QueryString.Value;\n        context.Request.Query.TryGetValue(\"CustomerId\", out var customerId);\n        context.Request.Query.TryGetValue(\"LocationId\", out var locationId);\n        context.Request.Query.TryGetValue(\"UserType\", out var userType);\n        context.Request.Query.TryGetValue(\"UserId\", out var userId);\n        MapStatus_ResponseBody = _ => $\"CustomerId:{customerId} LocationId:{locationId} UserType:{userType} UserId:{userId}\";\n        return base.MapStatus(context);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Transformations/HeaderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Middleware;\nusing System.Net.Sockets;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.Transformations;\n\n/// <summary>\n/// Ocelot feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/headerstransformation.rst\">Headers Transformation</see>.\n/// <para>Read the Docs: <see href=\"https://ocelot.readthedocs.io/en/develop/features/headerstransformation.html\">Headers Transformation</see>.</para>\n/// </summary>\n[Trait(\"Feat\", \"204\")] // https://github.com/ThreeMammals/Ocelot/pull/204\npublic sealed class HeaderTests : Steps\n{\n    private static FileHttpHandlerOptions DoNotAllowAutoRedirect => new() { AllowAutoRedirect = false };\n    private static FileHttpHandlerOptions UseCookieContainer => new() { UseCookieContainer = true };\n    private static FileHttpHandlerOptions DoNotUseCookieContainer => new() { UseCookieContainer = false };\n\n    [Fact]\n    public void Should_transform_upstream_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.UpstreamHeaderTransform.Add(\"Laz\", \"D, GP\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, \"Laz\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(\"Laz\", \"D\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Laz: GP\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_transform_downstream_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.DownstreamHeaderTransform.Add(\"Location\", \"http://www.bbc.co.uk/, http://ocelot.net/\");\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.OK, \"Location\", \"http://www.bbc.co.uk/\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseHeaderIs(\"Location\", \"http://ocelot.net/\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"190\")] // https://github.com/ThreeMammals/Ocelot/issues/190\n    public void Should_fix_issue_190()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.DownstreamHeaderTransform.Add(\"Location\", $\"{DownstreamUrl(port)}, {{BaseUrl}}\");\n        route.HttpHandlerOptions = DoNotAllowAutoRedirect;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, \"Location\", $\"{DownstreamUrl(port)}/pay/Receive\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n#if NET10_0_OR_GREATER\n            .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect(\"/\", DoNotAllowAutoRedirect.AllowAutoRedirect.Value))\n#else\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n#endif\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect))\n            .And(x => ThenTheResponseHeaderIs(\"Location\", \"http://localhost:5000/pay/Receive\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"205\")] // https://github.com/ThreeMammals/Ocelot/issues/205\n    public void Should_fix_issue_205()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.DownstreamHeaderTransform.Add(\"Location\", \"{DownstreamBaseUrl}, {BaseUrl}\");\n        route.HttpHandlerOptions = DoNotAllowAutoRedirect;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, \"Location\", $\"{DownstreamUrl(port)}/pay/Receive\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n#if NET10_0_OR_GREATER\n            .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect(\"/\", DoNotAllowAutoRedirect.AllowAutoRedirect.Value))\n#else\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n#endif\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect))\n            .And(x => ThenTheResponseHeaderIs(\"Location\", \"http://localhost:5000/pay/Receive\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"417\")] // https://github.com/ThreeMammals/Ocelot/issues/417\n    public void Should_fix_issue_417()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.DownstreamHeaderTransform.Add(\"Location\", \"{DownstreamBaseUrl}, {BaseUrl}\");\n        route.HttpHandlerOptions = DoNotAllowAutoRedirect;\n        var configuration = GivenConfiguration(route);\n        configuration.GlobalConfiguration.BaseUrl = \"http://anotherapp.azurewebsites.net\";\n\n        this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, \"Location\", $\"{DownstreamUrl(port)}/pay/Receive\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n#if NET10_0_OR_GREATER\n            .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect(\"/\", DoNotAllowAutoRedirect.AllowAutoRedirect.Value))\n#else\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n#endif\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect))\n            .And(x => ThenTheResponseHeaderIs(\"Location\", \"http://anotherapp.azurewebsites.net/pay/Receive\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"274\")] // https://github.com/ThreeMammals/Ocelot/issues/274\n    public void Request_should_reuse_cookies_with_cookie_container()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, \"/sso/{everything}\", \"/sso/{everything}\");\n        route.UpstreamHttpMethod = [HttpMethods.Get, HttpMethods.Post, HttpMethods.Options];\n        route.HttpHandlerOptions = UseCookieContainer;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/sso/test\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => WhenIGetUrlOnTheApiGateway(\"/sso/test\"))\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseHeaderIs(\"Set-Cookie\", \"test=0; path=/\"))\n            .And(x => GivenIAddCookieToMyRequest(\"test=1; path=/\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/sso/test\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"274\")] // https://github.com/ThreeMammals/Ocelot/issues/274\n    public void Request_should_have_own_cookies_no_cookie_container()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(port, \"/sso/{everything}\", \"/sso/{everything}\");\n        route.UpstreamHttpMethod = [HttpMethods.Get, HttpMethods.Post, HttpMethods.Options];\n        route.HttpHandlerOptions = DoNotUseCookieContainer;\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/sso/test\", HttpStatusCode.OK))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => WhenIGetUrlOnTheApiGateway(\"/sso/test\"))\n            .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseHeaderIs(\"Set-Cookie\", \"test=0; path=/\"))\n            .And(x => GivenIAddCookieToMyRequest(\"test=1; path=/\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/sso/test\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"474\")] // https://github.com/ThreeMammals/Ocelot/issues/474\n    [Trait(\"PR\", \"483\")] // https://github.com/ThreeMammals/Ocelot/pull/483\n    public void Issue_474_should_not_put_spaces_in_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, \"Accept\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(\"Accept\", \"text/html,application/xhtml+xml,application/xml;\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Accept: text/html,application/xhtml+xml,application/xml;\"))\n            .BDDfy();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"474\")]\n    [Trait(\"PR\", \"483\")]\n    public void Issue_474_should_put_spaces_in_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        var configuration = GivenConfiguration(route);\n\n        this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, \"Accept\"))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .And(x => GivenIAddAHeader(\"Accept\", \"text/html\"))\n            .And(x => GivenIAddAHeader(\"Accept\", \"application/xhtml+xml\"))\n            .And(x => GivenIAddAHeader(\"Accept\", \"application/xml\"))\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(x => ThenTheResponseBodyShouldBe(\"Accept: text/html, application/xhtml+xml, application/xml\"))\n            .BDDfy();\n    }\n\n    [Fact(DisplayName = \"TODO Redevelop Placeholders as part of Header Transformation feat\")]\n    [Trait(\"Feat\", \"623\")] // https://github.com/ThreeMammals/Ocelot/issues/623\n    [Trait(\"PR\", \"632\")] // https://github.com/ThreeMammals/Ocelot/pull/632\n    public async Task Should_pass_remote_ip_address_if_as_x_forwarded_for_header()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenDefaultRoute(port);\n        route.UpstreamHeaderTransform.Add(X_Forwarded_For, \"{RemoteIpAddress}\");\n        route.HttpHandlerOptions = DoNotAllowAutoRedirect;\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, X_Forwarded_For);\n        var ocelotIP = string.Empty;\n        var pipeline = new OcelotPipelineConfiguration\n        {\n            PreErrorResponderMiddleware = async (ctx, next) =>\n            {\n                ocelotIP = GetRemoteIpAddress(ctx);\n                await next.Invoke();\n            },\n        };\n        await GivenOcelotIsRunningAsync(pipeline);\n\n        //var remoteIpAddress = Dns.GetHostAddresses(\"dns.google\").First(a => a.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6).ToString();\n        //GivenIAddAHeader(X_Forwarded_For, remoteIpAddress);\n        await WhenIGetUrlOnTheApiGateway(\"/\");\n        ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n        await ThenTheResponseBodyShouldBeAsync(\"X-Forwarded-For: \" + /*remoteIpAddress*/ocelotIP);\n    }\n\n    public const string X_Forwarded_For = \"X-Forwarded-For\";\n    public const string X_Forwarded_Host = \"X-Forwarded-Host\";\n    public const string X_Forwarded_Proto = \"X-Forwarded-Proto\";\n\n    [Fact]\n    [Trait(\"Feat\", \"1658\")] // https://github.com/ThreeMammals/Ocelot/issues/1658\n    [Trait(\"PR\", \"1659\")] // https://github.com/ThreeMammals/Ocelot/pull/1659\n    public async Task ShouldApplyGlobalUpstreamHeaderTransformsForAllRoutes()\n    {\n        const string Ot_Route = \"Ot-Route\";\n        var port1 = PortFinder.GetRandomPort();\n        var route1 = GivenRoute(port1, \"/route1\");\n        route1.UpstreamHeaderTransform.Add(Ot_Route, \"Raman\");\n        var port2 = PortFinder.GetRandomPort();\n        var route2 = GivenRoute(port2, \"/route2\");\n        route2.UpstreamHeaderTransform.Add(Ot_Route, \"Mark\");\n        var port3 = PortFinder.GetRandomPort();\n        var route3 = GivenRoute(port3, \"/route3\");\n        var configuration = GivenConfiguration(route1, route2, route3);\n        configuration.GlobalConfiguration.BaseUrl = \"http://ocelot.net\";\n        configuration.GlobalConfiguration.UpstreamHeaderTransform = new Dictionary<string, string>()\n        {\n            { X_Forwarded_For, \"{RemoteIpAddress}\" },\n            { X_Forwarded_Host, \"{BaseUrl}\" },\n            { X_Forwarded_Proto, \"https\" },\n            { Ot_Route, \"?\" },\n        };\n        var allHeaders = configuration.GlobalConfiguration.UpstreamHeaderTransform.Keys\n            .Union(route1.UpstreamHeaderTransform.Keys.Intersect(route2.UpstreamHeaderTransform.Keys))\n            .ToArray();\n        GivenThereIsAServiceEchoingAHeader(port1, HttpStatusCode.OK, allHeaders);\n        GivenThereIsAServiceEchoingAHeader(port2, HttpStatusCode.OK, allHeaders);\n        GivenThereIsAServiceEchoingAHeader(port3, HttpStatusCode.OK, allHeaders);\n        GivenThereIsAConfiguration(configuration);\n\n        var ocelotIP = string.Empty;\n        var pipeline = new OcelotPipelineConfiguration\n        {\n            PreErrorResponderMiddleware = async (ctx, next) =>\n            {\n                ocelotIP = GetRemoteIpAddress(ctx);\n                await next.Invoke();\n            },\n        };\n        await GivenOcelotIsRunningAsync(pipeline);\n\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        ThenTheResponseBodyShouldBe(@$\"X-Forwarded-For: {ocelotIP}\nX-Forwarded-Host: http://ocelot.net\nX-Forwarded-Proto: https\nOt-Route: Raman\");\n\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        ThenTheResponseBodyShouldBe(@$\"X-Forwarded-For: {ocelotIP}\nX-Forwarded-Host: http://ocelot.net\nX-Forwarded-Proto: https\nOt-Route: Mark\");\n\n        await WhenIGetUrlOnTheApiGateway(\"/route3\");\n        ThenTheResponseBodyShouldBe(@$\"X-Forwarded-For: {ocelotIP}\nX-Forwarded-Host: http://ocelot.net\nX-Forwarded-Proto: https\nOt-Route: ?\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1658\")] // https://github.com/ThreeMammals/Ocelot/issues/1658\n    [Trait(\"PR\", \"1659\")] // https://github.com/ThreeMammals/Ocelot/pull/1659\n    public async Task ShouldApplyGlobalDownstreamHeaderTransformsForAllRoutes()\n    {\n        const string Who = \"Who\", X_Forwarded_By = \"X-Forwarded-By\";\n        var port1 = PortFinder.GetRandomPort();\n        var route1 = GivenRoute(port1, \"/route1\");\n        route1.DownstreamHeaderTransform.Add(Who, \"Raman, Mark\");\n        var port2 = PortFinder.GetRandomPort();\n        var route2 = GivenRoute(port2, \"/route2\");\n        route2.DownstreamHeaderTransform.Add(Who, \"Mark, Raman\");\n        var port3 = PortFinder.GetRandomPort();\n        var route3 = GivenRoute(port3, \"/route3\");\n        var configuration = GivenConfiguration(route1 ,route2, route3);\n        configuration.GlobalConfiguration.BaseUrl = \"http://ocelot.net\";\n        configuration.GlobalConfiguration.DownstreamHeaderTransform = new Dictionary<string, string>()\n        {\n            { X_Forwarded_By, \"{BaseUrl}\" },\n            { Who, \"HideMe, ?\" },\n        };\n        GivenThereIsAServiceReturningAHeaderBack(port1, HttpStatusCode.OK, Who, \"Raman Mark Raman\");\n        GivenThereIsAServiceReturningAHeaderBack(port2, HttpStatusCode.OK, Who, \"Mark Raman Mark\");\n        GivenThereIsAServiceReturningAHeaderBack(port3, HttpStatusCode.OK, Who, \"HideMe Mark\");\n        GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning();\n\n        await WhenIGetUrlOnTheApiGateway(\"/route1\");\n        ThenTheResponseHeaderExists(Who);\n        ThenTheResponseHeaderIs(Who, \"Mark Mark Mark\");\n        ThenTheResponseHeaderExists(X_Forwarded_By);\n        ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl);\n        await WhenIGetUrlOnTheApiGateway(\"/route2\");\n        ThenTheResponseHeaderIs(Who, \"Raman Raman Raman\");\n        ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl);\n        await WhenIGetUrlOnTheApiGateway(\"/route3\");\n        ThenTheResponseHeaderIs(Who, \"? Mark\");\n        ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl);\n    }\n\n    private static string GetRemoteIpAddress(HttpContext context)\n    {\n        var ip = context.Connection.RemoteIpAddress\n                ?? Dns.GetHostAddresses(string.Empty).FirstOrDefault(a => a.AddressFamily != AddressFamily.InterNetworkV6);\n        return ip.ToString();\n    }\n\n    private int _count;\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode)\n    {\n        Task MapCookies(HttpContext context)\n        {\n            if (_count == 0)\n            {\n                context.Response.Cookies.Append(\"test\", \"0\");\n                _count++;\n                context.Response.StatusCode = (int)statusCode;\n                return Task.CompletedTask;\n            }\n            else if (context.Request.Cookies.TryGetValue(\"test\", out var cookieValue))\n            {\n                if (cookieValue == \"0\")\n                {\n                    context.Response.StatusCode = (int)statusCode;\n                    return Task.CompletedTask;\n                }\n            }\n            else if (context.Request.Headers.TryGetValue(\"Set-Cookie\", out var headerValue))\n            {\n                if (headerValue == \"test=1; path=/\")\n                {\n                    context.Response.StatusCode = (int)statusCode;\n                    return Task.CompletedTask;\n                }\n            }\n            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;\n            return Task.CompletedTask;\n        }\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapCookies);\n    }\n\n    private void GivenThereIsAServiceReturningAHeaderBack(int port, HttpStatusCode statusCode, string headerKey, string headerValue)\n    {\n        Task MapHeaderIntoResponseHeaders(HttpContext context)\n        {\n            context.Response.OnStarting(() =>\n            {\n                context.Response.Headers.Append(headerKey, headerValue);\n                context.Response.StatusCode = (int)statusCode;\n                return Task.CompletedTask;\n            });\n            return Task.CompletedTask;\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapHeaderIntoResponseHeaders);\n    }\n\n    private void GivenThereIsAServiceEchoingAHeader(int port, HttpStatusCode statusCode, params string[] headerKeys)\n    {\n        Task MapHeaderIntoResponseBody(HttpContext context)\n        {\n            var body = new StringBuilder();\n            foreach (var key in headerKeys)\n            {\n                if (context.Request.Headers.TryGetValue(key, out var values))\n                {\n                    body.AppendLine($\"{key}: {values}\");\n                }\n            }\n            context.Response.StatusCode = (int)statusCode;\n            body.Length -= Environment.NewLine.Length;\n            return context.Response.WriteAsync(body.ToString());\n        }\n        handler.GivenThereIsAServiceRunningOn(port, MapHeaderIntoResponseBody);\n    }\n\n    /// <summary>\n    /// Using HttpClientHandler to configure auto-redirect behavior\n    /// </summary>\n    private async Task WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect(string url, bool allowAutoRedirect)\n    {\n        var handler = new HttpClientHandler()\n        {\n            AllowAutoRedirect = allowAutoRedirect,\n        };\n        var baseAddr = ocelotClient.BaseAddress;\n        ocelotClient = new(handler)\n        {\n            BaseAddress = baseAddr,\n        };\n        response = await ocelotClient.GetAsync(url);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Transformations/MethodTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.AcceptanceTests.Transformations;\n\npublic sealed class MethodTests : Steps\n{\n    public MethodTests()\n    {\n    }\n\n    [Fact]\n    public void Should_return_response_200_when_get_converted_to_post()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRouteWithMethods(port);\n        var configuration = GivenConfiguration(route);\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpMethods.Post))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\"))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_when_get_converted_to_post_with_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRouteWithMethods(port);\n        var configuration = GivenConfiguration(route);\n        const string expected = \"here is some content\";\n        var httpContent = new StringContent(expected);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpMethods.Post))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIGetUrlOnTheApiGateway(\"/\", httpContent))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe(expected))\n            .BDDfy();\n    }\n\n    [Fact]\n    public void Should_return_response_200_when_get_converted_to_get_with_content()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRouteWithMethods(port, HttpMethods.Post, HttpMethods.Get);\n        var configuration = GivenConfiguration(route);\n        const string expected = \"here is some content\";\n        var httpContent = new StringContent(expected);\n\n        this.Given(x => x.GivenThereIsAServiceRunningOn(port, \"/\", HttpMethods.Get))\n            .And(x => GivenThereIsAConfiguration(configuration))\n            .And(x => GivenOcelotIsRunning())\n            .When(x => WhenIPostUrlOnTheApiGateway(\"/\", httpContent))\n            .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))\n            .And(_ => ThenTheResponseBodyShouldBe(expected))\n            .BDDfy();\n    }\n\n    private FileRoute GivenRouteWithMethods(int port, string up = null, string down = null) => new()\n    {\n        DownstreamPathTemplate = \"/{url}\",\n        DownstreamScheme = Uri.UriSchemeHttp,\n        UpstreamPathTemplate = \"/{url}\",\n        UpstreamHttpMethod = [up ?? HttpMethods.Get],\n        DownstreamHostAndPorts = [ Localhost(port) ],\n        DownstreamHttpMethod = down ?? HttpMethods.Post,\n    };\n\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, string expected)\n    {\n        async Task MapMethod(HttpContext context)\n        {\n            if (context.Request.Method == expected)\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.OK;\n                var reader = new StreamReader(context.Request.Body);\n                var body = await reader.ReadToEndAsync();\n                await context.Response.WriteAsync(body);\n            }\n            else\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;\n            }\n        }\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapMethod);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using Moq;\nglobal using Ocelot;\nglobal using Ocelot.Testing;\nglobal using Ocelot.Testing.Boxing;\nglobal using Shouldly;\nglobal using System.Net;\nglobal using TestStack.BDDfy;\nglobal using Xunit;\n\nusing System.Diagnostics.CodeAnalysis;\n\n[assembly: SuppressMessage(\"Usage\", \"xUnit1004:Test methods should not be skipped\", Justification = \"Reviewed.\")]\n\ninternal class Usings { }\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.AcceptanceTests.Logging;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Logging;\nusing Ocelot.WebSockets;\nusing System.Net.WebSockets;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.WebSockets;\n\npublic sealed class ClientWebSocketTests : WebSocketsSteps\n{\n    private readonly ClientWebSocket _ws = new();\n    private readonly CancellationTokenSource _cts = new();\n\n    public ClientWebSocketTests()\n    {\n        _cts.CancelAfter(3_500); // run (wait) all tests no more than 3.5 seconds\n    }\n\n    public override void Dispose()\n    {\n        _ws.Dispose();\n        _cts.Dispose();\n        base.Dispose();\n    }\n\n    /// <summary>It tests the following stack: HTTP 1.1, SSL, WebSocket.</summary>\n    /// <returns>A <see cref=\"Task\"/> object.</returns>\n    [Theory]\n    [InlineData(\"ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx\", null)] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets\n    /* [InlineData(\"wss://echo.websocket.org\", \"Request served by \")] // https://websocket.org/tools/websocket-echo-server/ */\n    [InlineData(\"wss://ws.postman-echo.com/raw\", null)] // https://blog.postman.com/introducing-postman-websocket-echo-service/\n    public async Task Http11CLient_DirectConnection_ShouldConnect(string url, string expected)\n    {\n        GivenOptions();\n\n        var echoEndpoint = new Uri(url);\n        await _ws.ConnectAsync(echoEndpoint, _cts.Token);\n        var actual = await WhenISendAndReceiveEchoMessage();\n\n        if (string.IsNullOrEmpty(expected))\n            Assert.Equal(Expected(), actual);\n        else\n            Assert.StartsWith(expected, actual);\n    }\n\n    /// <summary>It tests the following stack: HTTP/2, SSL, WebSocket.</summary>\n    /// <remarks>HTTP/2 always requires an SSL certificate.</remarks>\n    /// <returns>A <see cref=\"Task\"/> object.</returns>\n    [Fact]\n    public async Task Http20CLient_DirectConnection_ShouldConnect()\n    {\n        int port = PortFinder.GetRandomPort();\n        await GivenWebSocketsHttp2ServiceIsRunningAsync(port, EchoAsync, _cts.Token);\n        GivenHttp2Options();\n\n        var echoEndpoint = new UriBuilder(Uri.UriSchemeWss, /*\"localhost\"*/ \"threemammals.com\", port).Uri;\n        using var handler = new HttpClientHandler\n        {\n            // ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,\n            PreAuthenticate = false,\n            Credentials = new NetworkCredential(\"tom@threemammals.com\", \"password\"),\n        };\n        if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS\n            handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; // TODO Makes sense to log in console\n\n        using var invoker = new HttpMessageInvoker(handler);\n        await _ws.ConnectAsync(echoEndpoint, invoker, _cts.Token);\n\n        var actual = await WhenISendAndReceiveEchoMessage();\n\n        Assert.Equal(Expected(), actual);\n    }\n\n    ///// <summary>In the middle, Ocelot tests the following stack: HTTP 1.1, SSL, WebSocket.</summary>\n    ///// <returns>A <see cref=\"Task\"/> object.</returns>\n    [Theory]\n    [InlineData(\"ws\", \"corefx-net-http11.azurewebsites.net\", 80, \"/WebSocket/EchoWebSocket.ashx\", null)] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets\n    /*[InlineData(\"wss\",\"echo.websocket.org\", 443, \"/\", \"Request served by \")] // https://websocket.org/tools/websocket-echo-server/ */\n    [InlineData(\"wss\", \"ws.postman-echo.com\", 443, \"/raw\", null)] // https://blog.postman.com/introducing-postman-websocket-echo-service/\n    public async Task Http11CLient_OcelotInTheMiddle_ShouldConnect(string scheme, string host, int port, string path, string expected)\n    {\n        var route = GivenWsRoute(scheme, host, port, path);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        int ocelotPort = PortFinder.GetRandomPort();\n        await StartOcelotWithWebSockets(ocelotPort, WithAddOcelot);\n        GivenOptions();\n\n        var ocelot = new UriBuilder(Uri.UriSchemeWs, \"localhost\", ocelotPort).Uri;\n        await _ws.ConnectAsync(ocelot, _cts.Token);\n        var actual = await WhenISendAndReceiveEchoMessage();\n\n        if (string.IsNullOrEmpty(expected))\n            Assert.Equal(Expected(), actual);\n        else\n            Assert.StartsWith(expected, actual);\n    }\n\n    /// <summary>In the middle, Ocelot tests the following stack: HTTP/2, SSL, WebSocket.</summary>\n    /// <remarks>HTTP/2 always requires an SSL certificate.<br/>\n    /// TODO: Scenario of HTTP/2 (SSL) vs WebSocket is not supported by Ocelot's <see cref=\"WebSocketsProxyMiddleware\"/>: see the ConnectAsync method.\n    /// </remarks>\n    /// <returns>A <see cref=\"Task\"/> object.</returns>\n    // Scenario of HTTP/2 (SSL) vs WebSocket is not supported by Ocelot's WebSocketsProxyMiddleware.\n    // AI Q.1: websocket http/2 | What browsers and tools support this couple?\n    // AI A.1: Proxy Servers: Implementing WebSocket support for HTTP/2 proxies requires handling the CONNECT request with a ':protocol' pseudo-header.\n    //         See Stack Overflow | Implementing websocket support for HTTP/2 proxies -> https://stackoverflow.com/questions/65129151/implementing-websocket-support-for-http-2-proxies\n    // AI Q.2: C# Yarp | Does Yarp support webSocket+HTTP/2 forwarding?\n    // AI A.2: Yes! YARP (Yet Another Reverse Proxy) supports WebSockets over HTTP/2 starting in .NET 7 and YARP 2.0.\n    //         See MS Learn | YARP Proxying WebSockets and SPDY -> https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/websockets\n    [Fact(DisplayName = \"TODO \" + nameof(Http20CLient_OcelotInTheMiddle_ShouldConnect),\n        Skip = \"TODO: HTTP/2 (SSL) vs WebSocket is unsupported scenario by Ocelot Core currently, unfortunately...\")]\n    public async Task Http20CLient_OcelotInTheMiddle_ShouldConnect()\n    {\n        var port = PortFinder.GetRandomPort();\n        await GivenWebSocketsHttp2ServiceIsRunningAsync(port, EchoAsync, _cts.Token);\n\n        var route = GivenWsRoute(Uri.UriSchemeWss, /*\"localhost\"*/ \"threemammals.com\", port);\n        route.DownstreamHttpVersion = HttpVersion.Version20.ToString(); // 2.0 !!!\n        route.DownstreamHttpVersionPolicy = nameof(HttpVersionPolicy.RequestVersionOrHigher);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        int ocelotPort = PortFinder.GetRandomPort();\n        await StartHttp2OcelotWithWebSockets(ocelotPort);\n        GivenHttp2Options();\n\n        var ocelot = new UriBuilder(Uri.UriSchemeWss, \"localhost\", ocelotPort).Uri;\n        using var handler = new HttpClientHandler\n        {\n            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,\n        };\n        using var invoker = new HttpMessageInvoker(handler);\n        await _ws.ConnectAsync(ocelot, invoker, _cts.Token); // TODO System.Net.WebSockets.WebSocketException : The server returned status code '500' when status code '200' was expected.\n\n        var actual = await WhenISendAndReceiveEchoMessage();\n        Assert.Equal(Expected(), actual);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    [InlineData(\"ws\", \"corefx-net-http11.azurewebsites.net\", 80, \"/WebSocket/EchoWebSocket.ashx\")] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets\n    /*[InlineData(\"wss\", \"echo.websocket.org\", 443, \"/\")] // https://websocket.org/tools/websocket-echo-server/ */\n    [InlineData(\"wss\", \"ws.postman-echo.com\", 443, \"/raw\")] // https://blog.postman.com/introducing-postman-websocket-echo-service/\n    public async Task Http11Client_ConnectionClosedPrematurely_ShouldCloseSocketsWithoutExceptions(string scheme, string host, int port, string path)\n    {\n        static void WithExtraLogging(IServiceCollection services) => services.AddOcelot()\n            .Services.RemoveAll<IOcelotLoggerFactory>()\n            .AddSingleton<IOcelotLoggerFactory, TestLoggerFactory<ClientWebSocketTests>>();\n\n        var route = GivenWsRoute(scheme, host, port, path);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        int ocelotPort = PortFinder.GetRandomPort();\n        await StartOcelotWithWebSockets(ocelotPort, WithExtraLogging);\n        GivenOptions();\n\n        var ocelot = new UriBuilder(Uri.UriSchemeWs, \"localhost\", ocelotPort).Uri;\n        await _ws.ConnectAsync(ocelot, _cts.Token);\n\n        //var ex = await WhenISendAndReceiveEchoMessage();\n        var upload = Encoding.UTF8.GetBytes(Expected());\n        await _ws.SendAsync(upload, WebSocketMessageType.Text, true, _cts.Token);\n        var echo = new byte[1024];\n        var result = await _ws.ReceiveAsync(echo, _cts.Token);\n\n        // Act\n        var exc = await Assert.ThrowsAsync<TaskCanceledException>(() =>\n        {\n            _cts.Cancel();\n            return _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, Expected() + \" has been sent\", _cts.Token);\n        });\n        _ws.Abort(); // !!! after cancellation of operations, let the connection be disposed aka finalized\n        await Task.Delay(1_000, Xunit.TestContext.Current.CancellationToken);\n\n        var factory = ocelotHost.Services.GetService<IOcelotLoggerFactory>();\n        var logger = (factory as TestLoggerFactory<ClientWebSocketTests>).Logger;\n        Assert.NotNull(logger);\n        logger.Messages.ShouldNotBeEmpty();\n\n        // STEPS TO REPRODUCE with old code, based on commit: https://github.com/ThreeMammals/Ocelot/commit/0b794b39e26d8bb538006eb5834b841c893c6611\n        // Bug930_StepsToReproduce(logger);\n        logger.Exceptions.ShouldBeEmpty(); // no errors on Ocelot's side, as they were swallowed in favor of logging a warning\n        logger.Messages.ShouldNotContain(m => m.Contains(Bug930RootCause)); // no bug\n        logger.Logbook.Contains(Bug930RootCause).ShouldBeFalse(); // no bug in the log\n        try\n        {\n            logger.Messages.ShouldContain(m => m.Contains(Bugfix930ExpectedMessage)); // logged warning\n            logger.Logbook.Contains(Bugfix930ExpectedMessage).ShouldBeTrue(); // logged warning\n        }\n        catch\n        {\n            // Be tolerant of attempted assertions, as they sometimes fail when the 'ConnectionClosedPrematurely' exception is not generated, thus the logbook is empty\n        }\n    }\n\n    public const string Bug930RootCause = \"The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseReceived'\";\n    public const string Bugfix930ExpectedMessage = \"WebSocketException when WebSocketErrorCode is ConnectionClosedPrematurely\";\n    private static void Bug930_StepsToReproduce(MemoryLogger logger)\n    {\n        logger.Exceptions.ShouldNotBeEmpty();\n        string PrintExceptions() => string.Join(Environment.NewLine,\n            logger.Exceptions.Select(e => $\"{e.GetType().Name}: {e.Message}\"));\n        logger.Exceptions.ShouldContain(e => e.GetType() == typeof(WebSocketException), PrintExceptions());\n        logger.Exceptions.Count(e => e.GetType() == typeof(WebSocketException)).ShouldBe(1, PrintExceptions());\n        logger.Exceptions.Count.ShouldBe(1, PrintExceptions());\n        var ex = logger.Exceptions.First();\n        ex.ShouldBeOfType<WebSocketException>();\n        ex.Message.ShouldBe(Bug930RootCause);\n        logger.Messages.ShouldContain(m => m.Contains(Bug930RootCause));\n        logger.Logbook.Contains(Bug930RootCause).ShouldBeTrue();\n    }\n\n    private static string Expected([CallerMemberName] string testName = null)\n        => testName ?? nameof(ClientWebSocketTests);\n\n    private static FileRoute GivenWsRoute(string scheme, string host, int port, string downstreamPath = null) => new()\n    {\n        UpstreamPathTemplate = \"/\",\n        DownstreamPathTemplate = downstreamPath ?? \"/\",\n        DownstreamScheme = scheme ?? Uri.UriSchemeWs,\n        DownstreamHostAndPorts = [ new(host, port) ],\n    };\n\n    private void GivenOptions()\n    {\n#if NET9_0_OR_GREATER\n        // Keep-Alive strategy is PING/PONG\n        // KeepAliveInterval is a positive finite TimeSpan, -AND-\n        // KeepAliveTimeout is a positive finite TimeSpan\n        _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1);\n        _ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);\n#else\n        // Keep-Alive strategy is Unsolicited PONG\n        // KeepAliveInterval is a positive finite TimeSpan, -AND-\n        // KeepAliveTimeout is TimeSpan.Zero or Timeout.InfiniteTimeSpan\n        _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1);\n#endif\n    }\n\n    private void GivenHttp2Options()\n    {\n#if NET9_0_OR_GREATER\n        // Keep-Alive strategy is PING/PONG\n        // KeepAliveInterval is a positive finite TimeSpan, -AND-\n        // KeepAliveTimeout is a positive finite TimeSpan\n        _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1);\n        _ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);\n#else\n        // Keep-Alive strategy is Unsolicited PONG\n        // KeepAliveInterval is a positive finite TimeSpan, -AND-\n        // KeepAliveTimeout is TimeSpan.Zero or Timeout.InfiniteTimeSpan\n        _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1);\n#endif\n        _ws.Options.HttpVersion = HttpVersion.Version20; // !!!\n        _ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;\n    }\n\n    private async Task<string> WhenISendAndReceiveEchoMessage([CallerMemberName] string message = null)\n    {\n        var upload = Encoding.UTF8.GetBytes(message);\n        await _ws.SendAsync(upload, WebSocketMessageType.Text, true, _cts.Token);\n        var echo = new byte[1024];\n        var result = await _ws.ReceiveAsync(echo, _cts.Token);\n        string actual = Encoding.UTF8.GetString(echo, 0, result.Count);\n        await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, message + \" has been sent\", _cts.Token);\n        return actual;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs",
    "content": "using Ocelot.Configuration.File;\nusing Ocelot.LoadBalancer.Balancers;\n\nnamespace Ocelot.AcceptanceTests.WebSockets;\n\npublic sealed class WebSocketsFactoryTests : WebSocketsSteps\n{\n    [Fact]\n    [Trait(\"Feat\", \"212\")]\n    [Trait(\"PR\", \"273\")] // https://github.com/ThreeMammals/Ocelot/pull/273\n    public async Task ShouldProxyWebsocketInputToDownstreamService()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = GivenRoute(\"/ws\", port);\n        var configuration = GivenConfiguration(route);\n        GivenThereIsAConfiguration(configuration);\n        int ocelotPort = PortFinder.GetRandomPort();\n        var ocelotUrl = new UriBuilder(Uri.UriSchemeWs, \"localhost\", ocelotPort).Uri;\n        await StartOcelotWithWebSockets(ocelotPort, null);\n        await GivenWebSocketsServiceIsRunningAsync(port, \"/ws\", EchoAsync, CancellationToken.None);\n        await StartClient(ocelotUrl);\n        ThenTheReceivedCountIs(10);\n\n        void ThenTheReceivedCountIs(int count) => _firstRecieved.Count.ShouldBe(count);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"212\")]\n    [Trait(\"PR\", \"273\")] // https://github.com/ThreeMammals/Ocelot/pull/273\n    public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer()\n    {\n        int port1 = PortFinder.GetRandomPort();\n        int port2 = PortFinder.GetRandomPort();\n        var route = GivenRoute(\"/ws\", port1, port2);\n        route.LoadBalancerOptions = new(nameof(RoundRobin));\n        var configuration = GivenConfiguration(route);\n        int ocelotPort = PortFinder.GetRandomPort();\n        this.Given(_ => GivenThereIsAConfiguration(configuration))\n            .And(_ => StartOcelotWithWebSockets(ocelotPort, null))\n            .And(_ => GivenWebSocketsServiceIsRunningAsync(port1, \"/ws\", EchoAsync, CancellationToken.None))\n            .And(_ => GivenWebSocketsServiceIsRunningAsync(port2, \"/ws\", MessageAsync, CancellationToken.None))\n            .When(_ => WhenIStartTheClients(ocelotPort))\n            .Then(_ => ThenBothDownstreamServicesAreCalled())\n            .BDDfy();\n    }\n\n    private FileRoute GivenRoute(string downstream = null, params int[] ports) => new()\n    {\n        UpstreamPathTemplate = \"/\",\n        DownstreamPathTemplate = downstream ?? \"/ws\",\n        DownstreamScheme = Uri.UriSchemeWs,\n        DownstreamHostAndPorts = ports.Select(Localhost).ToList(),\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/WebSockets/WebSocketsSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.WebSockets;\nusing System.Net.WebSockets;\nusing System.Text;\n\nnamespace Ocelot.AcceptanceTests.WebSockets;\n\npublic class WebSocketsSteps : Steps\n{\n    private readonly WebSocketsFactory _factory = new();\n    private readonly List<string> _secondRecieved = new();\n    protected readonly List<string> _firstRecieved = new();\n\n    protected static void WithConsole(WebHostBuilderContext context, ILoggingBuilder logging) => logging\n        .AddConfiguration(context.Configuration.GetSection(\"Logging\"))\n        .AddConsole();\n    protected static void WithConfiguration(WebHostBuilderContext hosting, IConfigurationBuilder config) => config\n        .SetBasePath(hosting.HostingEnvironment.ContentRootPath);\n    protected static void WithoutServices(IServiceCollection services) { }\n\n    protected Task GivenWebSocketServiceIsRunningOnAsync(string url, Action<KestrelServerOptions> options, Func<HttpContext, Func<Task>, Task> middleware) =>\n        handler.GivenThereIsAServiceRunningOnAsync(url,\n            WithConfiguration,\n            WithConsole,\n            WithoutServices,\n            app => app.UseWebSockets().Use(middleware),\n            web => web.UseUrls(url).ConfigureKestrel(options).UseKestrel() // UseKestrelHttpsConfiguration()\n        );\n\n    protected void ThenBothDownstreamServicesAreCalled()\n    {\n        _firstRecieved.Count.ShouldBe(10);\n        _firstRecieved.ForEach(x => x.ShouldBe(\"test\"));\n        _secondRecieved.Count.ShouldBe(10);\n        _secondRecieved.ForEach(x => x.ShouldBe(\"chocolate\"));\n    }\n\n    protected Task GivenWebSocketsServiceIsRunningAsync(int port, string path, Func<WebSocket, CancellationToken, Task> webSocketHandler, CancellationToken token)\n    {\n        async Task TheMiddleware(HttpContext context, Func<Task> next)\n        {\n            if (context.Request.Path != path)\n            {\n                await next();\n                return;\n            }\n            if (context.WebSockets.IsWebSocketRequest)\n            {\n                var webSocket = await context.WebSockets.AcceptWebSocketAsync();\n                await webSocketHandler(webSocket, token);\n            }\n            else\n            {\n                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;\n            }\n        }\n        void NoOptions(KestrelServerOptions options)\n        {\n        }\n        var url = DownstreamUrl(port);\n        return GivenWebSocketServiceIsRunningOnAsync(url, NoOptions, TheMiddleware);\n    }\n\n    protected Task GivenWebSocketsHttp2ServiceIsRunningAsync(int port, Func<WebSocket, CancellationToken, Task> webSocketHandler, CancellationToken token)\n    {\n        async Task TheMiddleware(HttpContext context, Func<Task> next)\n        {\n            if (context.WebSockets.IsWebSocketRequest)\n            {\n                var webSocket = await context.WebSockets.AcceptWebSocketAsync();\n                await webSocketHandler(webSocket, token);\n            }\n            else\n            {\n                await next();\n                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;\n            }\n        }\n        void WithOptions(KestrelServerOptions options) => options.ListenAnyIP(port, WithHttp2);\n        var url = DownstreamUrl(port, Uri.UriSchemeHttps);\n        return GivenWebSocketServiceIsRunningOnAsync(url, WithOptions, TheMiddleware);\n    }\n\n    protected static async Task EchoAsync(WebSocket ws, CancellationToken token)\n    {\n        try\n        {\n            var buffer = new byte[1024 * 4];\n            WebSocketReceiveResult result;\n            while (true)\n            {\n                Array.Clear(buffer);\n                result = await ws.ReceiveAsync(buffer, token);\n                if (result.CloseStatus.HasValue)\n                    break;\n                var echo = new ArraySegment<byte>(buffer, 0, result.Count);\n                await ws.SendAsync(echo, result.MessageType, result.EndOfMessage, token);\n            }\n\n            await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, token);\n        }\n        catch (Exception e)\n        {\n            Console.WriteLine(e);\n        }\n    }\n\n    protected static async Task MessageAsync(WebSocket webSocket, CancellationToken token)\n    {\n        try\n        {\n            var buffer = new byte[1024 * 4];\n            var bytes = Encoding.UTF8.GetBytes(\"chocolate\");\n            var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);\n            while (!result.CloseStatus.HasValue)\n            {\n                await webSocket.SendAsync(new ArraySegment<byte>(bytes), result.MessageType, result.EndOfMessage, token);\n                result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);\n            }\n            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, token);\n        }\n        catch (Exception e)\n        {\n            Console.WriteLine(e);\n        }\n    }\n\n    protected async Task StartClient(Uri url)\n    {\n        var client = _factory.CreateClient();\n        await client.ConnectAsync(url, CancellationToken.None);\n\n        var sending = Task.Run(async () =>\n        {\n            var line = \"test\";\n            for (var i = 0; i < 10; i++)\n            {\n                var bytes = Encoding.UTF8.GetBytes(line);\n                await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);\n                await Task.Delay(10);\n            }\n            await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);\n        });\n\n        var receiving = Task.Run(async () =>\n        {\n            var buffer = new byte[1024 * 4];\n            while (true)\n            {\n                var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);\n                if (result.MessageType == WebSocketMessageType.Text)\n                {\n                    _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));\n                }\n                else if (result.MessageType == WebSocketMessageType.Close)\n                {\n                    if (client.State != WebSocketState.Closed)\n                    {\n                        // Last version, the client state is CloseReceived\n                        // Valid states are: Open, CloseReceived, CloseSent\n                        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);\n                    }\n                    break;\n                }\n            }\n        });\n\n        await Task.WhenAll(sending, receiving);\n    }\n\n    protected async Task StartSecondClient(Uri url)\n    {\n        await Task.Delay(500);\n        var client = _factory.CreateClient();\n        await client.ConnectAsync(url, CancellationToken.None);\n\n        var sending = Task.Run(async () =>\n        {\n            var line = \"test\";\n            for (var i = 0; i < 10; i++)\n            {\n                var bytes = Encoding.UTF8.GetBytes(line);\n                await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);\n                await Task.Delay(10);\n            }\n            await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);\n        });\n\n        var receiving = Task.Run(async () =>\n        {\n            var buffer = new byte[1024 * 4];\n            while (true)\n            {\n                var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);\n                if (result.MessageType == WebSocketMessageType.Text)\n                {\n                    _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));\n                }\n                else if (result.MessageType == WebSocketMessageType.Close)\n                {\n                    if (client.State != WebSocketState.Closed)\n                    {\n                        // Last version, the client state is CloseReceived\n                        // Valid states are: Open, CloseReceived, CloseSent\n                        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);\n                    }\n                    break;\n                }\n            }\n        });\n\n        await Task.WhenAll(sending, receiving);\n    }\n\n    protected Task WhenIStartTheClients(int port)\n    {\n        var url = new UriBuilder(Uri.UriSchemeWs, \"localhost\", port).Uri;\n        var firstClient = StartClient(url);\n        var secondClient = StartSecondClient(url);\n        return Task.WhenAll(firstClient, secondClient);\n    }\n\n    protected Task StartOcelotWithWebSockets(int port, Action<IServiceCollection> configureServices)\n        => StartOcelotWithWebSockets(port, Uri.UriSchemeHttp, configureServices);\n    protected Task StartOcelotWithWebSockets(int port, string scheme, Action<IServiceCollection> configureServices)\n    {\n        var url = DownstreamUrl(port, scheme);\n        void ConfigureWebHost(IWebHostBuilder b) => b\n            .UseUrls(url)\n            .ConfigureLogging(WithConsole);\n        return GivenOcelotHostIsRunning(WithBasicConfiguration, configureServices ?? WithAddOcelot,\n            WithWebSockets, null, ConfigureWebHost, null, null);\n    }\n\n    protected static void WithWebSockets(IApplicationBuilder app)\n        => app.UseWebSockets().UseOcelot().Wait();\n    protected static void WithHttp2(ListenOptions options)\n    {\n        options.Protocols = HttpProtocols.Http2;\n        options.UseHttps(\"mycert2.pfx\", \"password\");\n    }\n    protected Task StartHttp2OcelotWithWebSockets(int port)\n    {\n        void WithOptions(KestrelServerOptions o) => o.ListenAnyIP(port, WithHttp2);\n        var url = DownstreamUrl(port, Uri.UriSchemeHttps);\n        void ConfigureWebHost(IWebHostBuilder b) => b.UseUrls(url)\n            .ConfigureLogging(WithConsole)\n            .ConfigureKestrel(WithOptions).UseKestrel(); // UseKestrelHttpsConfiguration()\n        return GivenOcelotHostIsRunning(WithBasicConfiguration, WithAddOcelot, WithWebSockets, null, ConfigureWebHost, null, null);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/appsettings.json",
    "content": "﻿{\r\n  \"Logging\": {\r\n    \"IncludeScopes\": true,\r\n    \"LogLevel\": {\r\n      \"Default\": \"Error\",\r\n      \"System\": \"Error\",\r\n      \"Microsoft\": \"Error\"\r\n    }\r\n  },\r\n  \"spring\": {\r\n    \"application\": {\r\n      \"name\": \"ocelot\"\r\n    }\r\n  },\r\n  \"eureka\": {\r\n    \"client\": {\r\n      \"serviceUrl\": \"http://127.0.0.1:8761/eureka/\",\r\n      \"shouldRegisterWithEureka\": true,\r\n      \"shouldFetchRegistry\": true,\r\n      \"port\": 5000,\r\n      \"hostName\": \"localhost\"\r\n    }\r\n  }\r\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/appsettings.product.json",
    "content": "﻿{\n  \"Logging\": {\n    \"IncludeScopes\": true,\n    \"LogLevel\": {\n      \"Default\": \"Error\",\n      \"System\": \"Error\",\n      \"Microsoft\": \"Error\"\n    }\n  },\n  \"spring\": {\n    \"application\": {\n      \"name\": \"product\"\n    }\n  },\n  \"eureka\": {\n    \"client\": {\n      \"serviceUrl\": \"http://localhost:8761/eureka/\",\n      \"shouldRegisterWithEureka\": true\n    },\n    \"instance\": {\n      \"port\": 50371\n    }\n  }\n}\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/mycert2.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEHTCCAwWgAwIBAgIUKXS7AlTRlVtDBeMDxEVUrptJ3a8wDQYJKoZIhvcNAQEL\nBQAwgZ0xCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlCZXJrc2hpcmUxEzARBgNVBAcM\nCk1haWRlbmhlYWQxFTATBgNVBAoMDFRocmVlTWFtbWFsczEPMA0GA1UECwwGb3du\nZXJzMRkwFwYDVQQDDBB0aHJlZW1hbW1hbHMuY29tMSIwIAYJKoZIhvcNAQkBFhNk\nb3RuZXQwNDRAZ21haWwuY29tMB4XDTI1MTIxNTE1NDQ0MVoXDTI4MDkxMDE1NDQ0\nMVowgZ0xCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlCZXJrc2hpcmUxEzARBgNVBAcM\nCk1haWRlbmhlYWQxFTATBgNVBAoMDFRocmVlTWFtbWFsczEPMA0GA1UECwwGb3du\nZXJzMRkwFwYDVQQDDBB0aHJlZW1hbW1hbHMuY29tMSIwIAYJKoZIhvcNAQkBFhNk\nb3RuZXQwNDRAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAp873B/a12uQgr8GLcceJCc5xQkGI7f5Zg7brhBpycQymP6bYs3+BNgetbsWL\nmGbXWs1xvbUXghYxziu/0sxo1aFvrcSO84FfyjCkWc2jxrcLf/VosKobhd+Jo175\nEvnSUz+VapSWJKXWUSlEHmnGaE9FKFfs99/+PfHemJHixyVOg7ekc9kzVZ2/Fwn5\nEj7SS1PHB8vVuTZerztSyOgeGm3Kqh2+5kn7q48uIVRn9O2FaRh4MaSKZnYLM6T4\ngkdzctEjdqB0Tls+OqK/uAJ1hHJByoBhn1/b/meezp9jgLz/pUij3YI2zaBIA4Eg\njjzhUm8X44bL8ptSJVmEs+4HvwIDAQABo1MwUTAdBgNVHQ4EFgQUla6gxlMjuzI/\nFzdPuiA47ICdFB0wHwYDVR0jBBgwFoAUla6gxlMjuzI/FzdPuiA47ICdFB0wDwYD\nVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAR9vrWaZzTbL6gL422SLu\nIY3Y5w84uKN8QE5WpnbV1fOE5lq+fwHCsLwrz5dqXJZCuy5fAfrPvFCKbLE30DRU\nwN7ycXPVfInRA2xnqE8BPllyd110xCGWLh5/2cvSqPbcu+U1VTjkvAWzKz2VS3OT\nRPYhyp9/lyiMoq813rzIykhFHYYfuDBig1R8qoYRlGXWn7mTdttjouzRdBBce3BS\nmtQWpwSNBbheWq2GN5RzKo42+atW25jF7HaAKHzgAdqbBb3tQo9YN0BcTlR44IIZ\neTzf9UWCqu3zmtHvrX74JU2TPfE9L/sZlN9Dh/fHDwc8IIEDxP624P3x6V77c7Dt\nQA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/Ocelot.AcceptanceTests/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==\"\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Serilog\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[4.3.1, )\",\n        \"resolved\": \"4.3.1\",\n        \"contentHash\": \"savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==\"\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"TestStack.BDDfy\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.9.120-beta, )\",\n        \"resolved\": \"8.0.9.120-beta\",\n        \"contentHash\": \"Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==\"\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[10.0.5, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net10.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net8.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.25, )\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.25, )\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Serilog\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[4.3.1, )\",\n        \"resolved\": \"4.3.1\",\n        \"contentHash\": \"savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==\"\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"TestStack.BDDfy\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.9.120-beta, )\",\n        \"resolved\": \"8.0.9.120-beta\",\n        \"contentHash\": \"Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==\"\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"8.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\",\n        \"dependencies\": {\n          \"System.Text.Encodings.Web\": \"10.0.0\",\n          \"System.Text.Json\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"7.1.2\",\n          \"System.IdentityModel.Tokens.Jwt\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[8.0.25, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net8.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[9.0.14, )\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[9.0.14, )\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==\"\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Serilog\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[4.3.1, )\",\n        \"resolved\": \"4.3.1\",\n        \"contentHash\": \"savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==\"\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.3.0, )\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"TestStack.BDDfy\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.9.120-beta, )\",\n        \"resolved\": \"8.0.9.120-beta\",\n        \"contentHash\": \"Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==\"\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"9.0.3\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\",\n        \"dependencies\": {\n          \"System.Text.Encodings.Web\": \"10.0.0\",\n          \"System.Text.Json\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[9.0.14, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs",
    "content": "using Microsoft.AspNetCore.Hosting;\r\nusing Microsoft.Extensions.Configuration;\r\nusing Microsoft.Extensions.Logging;\r\nusing System.Net;\n\r\nnamespace Ocelot.Benchmarks;\r\n\r\n[Config(typeof(AllTheThingsBenchmarks))]\r\npublic sealed class AllTheThingsBenchmarks : ManualConfig, IDisposable\r\n{\r\n    private readonly BenchmarkSteps steps = new();\r\n    public void Dispose() => steps.Dispose();\r\n\r\n    public AllTheThingsBenchmarks()\r\n    {\r\n        AddColumn(StatisticColumn.AllStatistics);\r\n        AddDiagnoser(MemoryDiagnoser.Default);\r\n        AddValidator(BaselineValidator.FailOnError);\r\n    }\r\n\r\n    [GlobalSetup]\r\n    public void SetUp()\r\n    {\r\n        var port = PortFinder.GetRandomPort();\r\n        var route = steps.GivenDefaultRoute(port);\r\n        var configuration = steps.GivenConfiguration(route);\r\n        steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(AllTheThingsBenchmarks));\r\n        steps.GivenThereIsAConfiguration(configuration);\r\n        steps.GivenOcelotIsRunning(\r\n            host => host.ConfigureLogging(\r\n                (c, l) => l.AddConfiguration(c.Configuration.GetSection(\"Logging\"))) // Action<IWebHostBuilder> postConfigureHost,\r\n        );\r\n    }\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public Task Baseline()\r\n    {\r\n        return steps.WhenIGetUrlOnTheApiGateway(\"/\");\r\n    }\r\n\r\n    /* Summary\r\n            BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0]\r\n            Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores\r\n           .NET Core SDK = 2.1.4\r\n\r\n             [Host]     : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT\r\n             DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT\r\n                Method |     Mean |     Error |    StdDev |    StdErr |      Min |       Q1 |   Median |       Q3 |      Max |  Op/s | Scaled |   Gen 0 |  Gen 1 | Allocated |\r\n             --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:|\r\n              Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 |   1.00 | 31.2500 | 3.9063 |   1.63 KB |\r\n    */\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/BenchmarkSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.Benchmarks;\n\ninternal class BenchmarkSteps : AcceptanceSteps\n{\n    public void GivenThereIsAServiceRunningOnKestrel(int port, string basePath, int statusCode,\n        Action<KestrelServerOptions> configureKestrel,\n        [CallerMemberName] string responseBody = null)\n        => handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, configureKestrel,\n            context =>\n            {\n                context.Response.StatusCode = statusCode;\n                return context.Response.WriteAsync(responseBody);\n            });\n    public void GivenThereIsAServiceRunningOnKestrel(int port, string basePath, Action<KestrelServerOptions> configureKestrel, RequestDelegate @delegate)\n        => handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, configureKestrel, @delegate);\n\n    public int GivenOcelotIsRunning(Action<IWebHostBuilder> postConfigureHost)\n        => GivenOcelotIsRunning(null, null, null, null, postConfigureHost, null, null);\n    public int GivenOcelotIsRunning(Action<IApplicationBuilder> configureApp, Action<IWebHostBuilder> postConfigureHost)\n        => GivenOcelotIsRunning(null, null, configureApp, null, postConfigureHost, null, null);\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.DependencyInjection;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.DownstreamRouteFinder.Middleware;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.Benchmarks;\n\n[SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 5)]\n[Config(typeof(DownstreamRouteFinderMiddlewareBenchmarks))]\npublic class DownstreamRouteFinderMiddlewareBenchmarks : ManualConfig\n{\n    private DownstreamRouteFinderMiddleware _middleware;\n    private RequestDelegate _next;\n    private HttpContext _httpContext;\n\n    public DownstreamRouteFinderMiddlewareBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    [GlobalSetup]\n    public void SetUp()\n    {\n        var serviceCollection = new ServiceCollection();\n        var config = new ConfigurationRoot(new List<IConfigurationProvider>());\n        var builder = new OcelotBuilder(serviceCollection, config);\n        var services = serviceCollection.BuildServiceProvider(true);\n        var loggerFactory = services.GetService<IOcelotLoggerFactory>();\n        var drpf = services.GetService<IDownstreamRouteProviderFactory>();\n\n        _next = async context =>\n        {\n            await Task.CompletedTask;\n            throw new Exception(\"BOOM\");\n        };\n\n        _middleware = new DownstreamRouteFinderMiddleware(_next, loggerFactory, drpf);\n\n        var httpContext = new DefaultHttpContext\n        {\n            Request =\n            {\n                Path = new PathString(\"/test\"),\n                QueryString = new QueryString(\"?a=b\"),\n            },\n        };\n        httpContext.Request.Headers.Append(\"Host\", \"most\");\n        httpContext.Items.SetIInternalConfiguration(new InternalConfiguration());\n        _httpContext = httpContext;\n    }\n\n    [Benchmark(Baseline = true)]\n    public async Task Baseline()\n    {\n        await _middleware.Invoke(_httpContext);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Errors.Middleware;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\n\nnamespace Ocelot.Benchmarks;\n\n[SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 5)]\n[Config(typeof(ExceptionHandlerMiddlewareBenchmarks))]\npublic class ExceptionHandlerMiddlewareBenchmarks : ManualConfig\n{\n    private ExceptionHandlerMiddleware _middleware;\n    private RequestDelegate _next;\n    private HttpContext _httpContext;\n\n    public ExceptionHandlerMiddlewareBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    [GlobalSetup]\n    public void SetUp()\n    {\n        var serviceCollection = new ServiceCollection();\n        var config = new ConfigurationRoot(new List<IConfigurationProvider>());\n        var builder = new OcelotBuilder(serviceCollection, config);\n        var services = serviceCollection.BuildServiceProvider(true);\n        var loggerFactory = services.GetService<IOcelotLoggerFactory>();\n        var repo = services.GetService<IRequestScopedDataRepository>();\n\n        _next = async context =>\n        {\n            await Task.CompletedTask;\n            throw new Exception(\"BOOM\");\n        };\n\n        _middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, repo);\n        _httpContext = new DefaultHttpContext();\n    }\n\n    [Benchmark(Baseline = true)]\n    public async Task Baseline()\n    {\n        await _middleware.Invoke(_httpContext);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs",
    "content": "﻿using BenchmarkDotNet.Order;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Net;\n\nnamespace Ocelot.Benchmarks;\n\n[Config(typeof(MsLoggerBenchmarks))]\n[Orderer(SummaryOrderPolicy.FastestToSlowest)]\n[MaxIterationCount(16)]\npublic sealed class MsLoggerBenchmarks : ManualConfig, IDisposable\n{\n    private readonly BenchmarkSteps steps = new();\n    public void Dispose() => steps.Dispose();\n\n    public MsLoggerBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    private Task SendRequest()\n    {\n        return steps.WhenIGetUrlOnTheApiGateway(\"/\");\n    }\n\n    [Benchmark(Baseline = true)]\n    public async Task LogLevelCritical() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelCritical))]\n    public void SetUpCritical() => OcelotFactory(LogLevel.Critical);\n\n    [Benchmark]\n    public async Task LogLevelError() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelError))]\n    public void SetupError() => OcelotFactory(LogLevel.Error);\n\n    [Benchmark]\n    public async Task LogLevelWarning() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelWarning))]\n    public void SetUpWarning() => OcelotFactory(LogLevel.Warning);\n\n    [Benchmark]\n    public async Task LogLevelInformation() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelInformation))]\n    public void SetUpInformation() => OcelotFactory(LogLevel.Information);\n\n    [Benchmark]\n    public async Task LogLevelTrace() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelTrace))]\n    public void SetUpTrace() => OcelotFactory(LogLevel.Trace);\n\n    [GlobalCleanup(Targets = new[]\n    {\n        nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation),\n        nameof(LogLevelTrace),\n    })]\n    public void OcelotCleanup()\n    {\n        steps.Dispose();\n    }\n\n    [GlobalCleanup]\n    public void Cleanup()\n    {\n        Dispose();\n    }\n\n    private void GivenOcelotIsRunning(LogLevel minLogLevel)\n        => steps.GivenOcelotIsRunning(\n            async app =>\n            {\n                app.Use(async (context, next) =>\n                {\n                    var loggerFactory = context.RequestServices.GetService<IOcelotLoggerFactory>();\n                    var ocelotLogger = loggerFactory.CreateLogger<MsLoggerBenchmarks>();\n                    ocelotLogger.LogDebug(() => $\"DEBUG: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogTrace(() => $\"TRACE: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogInformation(() => $\"INFORMATION: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogWarning(() => $\"WARNING: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogError(() => $\"ERROR: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n                    ocelotLogger.LogCritical(() => $\"CRITICAL: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n\n                    await next.Invoke();\n                });\n                await app.UseOcelot();\n            }, // Action<IApplicationBuilder> configureApp\n            host => host.ConfigureLogging(\n                (c, l) => l.ClearProviders().SetMinimumLevel(minLogLevel).AddConsole()) // Action<IWebHostBuilder> postConfigureHost\n        );\n\n    private void OcelotFactory(LogLevel minLogLevel)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = steps.GivenDefaultRoute(port);\n        var configuration = steps.GivenConfiguration(route);\n        steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(MsLoggerBenchmarks));\n        steps.GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(minLogLevel);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <AssemblyName>Ocelot.Benchmarks</AssemblyName>\n    <OutputType>Exe</OutputType>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <NoWarn>$(NoWarn);CS0618;CS1591</NoWarn>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\testing\\Ocelot.Testing.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"BenchmarkDotNet\" Version=\"0.15.8\" />\n    <PackageReference Include=\"Serilog.AspNetCore\" Version=\"10.0.0\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/PayloadBenchmarks.cs",
    "content": "﻿using BenchmarkDotNet.Order;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing System.Diagnostics;\nusing System.Net.Http.Headers;\nusing System.Reflection;\nusing System.Text;\n\nnamespace Ocelot.Benchmarks;\n\n[Config(typeof(PayloadBenchmarks))]\n[Orderer(SummaryOrderPolicy.FastestToSlowest)]\npublic sealed class PayloadBenchmarks : ManualConfig, IDisposable\n{\n    private readonly BenchmarkSteps steps = new();\n    public void Dispose() => steps.Dispose();\n\n    private const string BasePayload =\n        \"{\\\"_id\\\":\\\"65789c1611a3b1feb49f9e65\\\",\\\"index\\\":0,\\\"guid\\\":\\\"6622d724-c17d-4939-9c68-158bf2dc5c57\\\",\\\"isActive\\\":false,\\\"balance\\\":\\\"$1,398.26\\\",\\\"picture\\\":\\\"http://placehold.it/32x32\\\",\\\"age\\\":33,\\\"eyeColor\\\":\\\"blue\\\",\\\"name\\\":\\\"WilkersonPayne\\\",\\\"gender\\\":\\\"male\\\",\\\"company\\\":\\\"NEOCENT\\\",\\\"email\\\":\\\"wilkersonpayne@neocent.com\\\",\\\"phone\\\":\\\"+1(837)588-3248\\\",\\\"address\\\":\\\"932BatchelderStreet,Campo,Texas,1310\\\",\\\"about\\\":\\\"Dolorsuntminimnullatemporlaboretempornostrudnon.Irureconsectetursintenimestadduissunttemporquisnisi.Laboreoccaecatculpaaliquaipsumreprehenderitadofficia.Sunteuutinpariaturanimofficia.CommodosintLoremametincididuntvelitesse.Nonaliquasintdoeiusmodexercitation.Suntcommododolorcupidatatculpareprehenderitfugiatexquisamet.\\\\r\\\\n\\\",\\\"registered\\\":\\\"2021-09-06T11:54:41-02:00\\\",\\\"latitude\\\":-45.256336,\\\"longitude\\\":164.343713,\\\"tags\\\":[\\\"cillum\\\",\\\"cupidatat\\\",\\\"aliquip\\\",\\\"culpa\\\",\\\"non\\\",\\\"laboris\\\",\\\"non\\\"],\\\"friends\\\":[{\\\"id\\\":0,\\\"name\\\":\\\"MistyMorton\\\"},{\\\"id\\\":1,\\\"name\\\":\\\"AraceliAcosta\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"WalterDelaney\\\"}],\\\"greeting\\\":\\\"Hello,WilkersonPayne!Youhave1unreadmessages.\\\",\\\"favoriteFruit\\\":\\\"strawberry\\\"}\";\n\n    public PayloadBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    [GlobalSetup]\n    public void SetUp()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = steps.GivenDefaultRoute(port);\n        var configuration = steps.GivenConfiguration(route);\n        steps.GivenThereIsAServiceRunningOnKestrel(port, \"/\", 201,\n            options => options.Limits.MaxRequestBodySize = 2684354561,\n            nameof(PayloadBenchmarks));\n        steps.GivenThereIsAConfiguration(configuration);\n        steps.GivenOcelotIsRunning(host => host\n            .ConfigureKestrel((_, options) => options.Limits.MaxRequestBodySize = 2684354561)\n            .ConfigureLogging((hosting, logging) => logging.AddConfiguration(hosting.Configuration.GetSection(\"Logging\"))));\n    }\n\n    [Benchmark(Baseline = true)]\n    [ArgumentsSource(nameof(Payloads))]\n    public async Task Baseline(string payLoadPath, string payloadName, bool isJson)\n    {\n        using var content = new StreamContent(File.OpenRead(payLoadPath));\n        content.Headers.ContentType = new MediaTypeHeaderValue(string.Concat(\"application/\", isJson ? \"json\" : \"octet-stream\"));\n        await steps.WhenIPostUrlOnTheApiGateway(\"/\", content);\n    }\n\n    /// <summary>\n    /// Generating the payloads for the benchmarks dynamically.\n    /// </summary>\n    /// <returns>The payloads containing path, file name and a boolean indicating if the file is a json or not.</returns>\n    public static IEnumerable<object[]> Payloads()\n    {\n        var baseDirectory = GetBaseDirectory();\n        var payloadsDirectory = Path.Combine(baseDirectory, nameof(Payloads));\n\n        if (!Directory.Exists(payloadsDirectory))\n        {\n            Directory.CreateDirectory(payloadsDirectory);\n        }\n\n        // Array of sizes in kilobytes for JSON files\n        var jsonSizes = new[] { 1, 16, 32, 64, 128, 256, 512, 2 * 1024, 8 * 1024, 15 * 1024, 30 * 1024 };\n        foreach (var size in jsonSizes)\n        {\n            yield return GeneratePayload(size, payloadsDirectory, $\"{size}KBPayload.json\", true);\n        }\n\n        // Array of sizes in megabytes for DAT files\n        var datSizes = new[] { 10, 100, 1024 };\n        foreach (var size in datSizes)\n        {\n            yield return GeneratePayload(size, payloadsDirectory, $\"{size}MBPayload.dat\", false);\n        }\n    }\n\n    private static string GetBaseDirectory()\n    {\n        var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);\n        Debug.Assert(baseDirectory != null, nameof(baseDirectory) + \" != null\");\n        return baseDirectory;\n    }\n\n    private static object[] GeneratePayload(int size, string directory, string fileName, bool isJson)\n    {\n        var filePath = Path.Combine(directory, fileName);\n        var generateDummy = isJson ? (Func<int, string, string>) GenerateDummyJsonFile : GenerateDummyDatFile;\n        return new object[]\n        {\n            generateDummy(size, filePath),\n            fileName,\n            isJson,\n        };\n    }\n\n    /// <summary>\n    /// Generates a dummy payload of the given size in KB.\n    /// The payload is a JSON array of the given size.\n    /// </summary>\n    /// <param name=\"sizeInKb\">The size in KB.</param>\n    /// <param name=\"payloadPath\">The payload path.</param>\n    /// <returns>The current payload path.</returns>\n    private static string GenerateDummyJsonFile(int sizeInKb, string payloadPath)\n    {\n        ArgumentNullException.ThrowIfNull(payloadPath);\n\n        if (File.Exists(payloadPath))\n        {\n            return payloadPath;\n        }\n\n        var targetSizeInBytes = sizeInKb * 1024L;\n\n        using var fileStream = new FileStream(payloadPath, FileMode.Create, FileAccess.Write);\n        using var streamWriter = new StreamWriter(fileStream);\n\n        var byteArrayLength = Encoding.UTF8.GetBytes(BasePayload).Length;\n        var firstObject = true;\n\n        streamWriter.Write(\"[\");\n        while (fileStream.Length < targetSizeInBytes - byteArrayLength)\n        {\n            if (!firstObject)\n            {\n                streamWriter.Write(\",\");\n            }\n            else\n            {\n                firstObject = false;\n            }\n\n            streamWriter.Write(BasePayload);\n        }\n\n        streamWriter.Write(\"]\");\n\n        return payloadPath;\n    }\n\n    /// <summary>\n    /// Generates a dummy payload of the given size in MB.\n    /// Avoiding maintaining a large file in the repository.\n    /// </summary>\n    /// <param name=\"sizeInMb\">The file size in MB.</param>\n    /// <param name=\"payloadPath\">The path to the payload file.</param>\n    /// <returns>The payload file path.</returns>\n    /// <exception cref=\"ArgumentNullException\">Throwing an exception if the payload path is null.</exception>\n    private static string GenerateDummyDatFile(int sizeInMb, string payloadPath)\n    {\n        ArgumentNullException.ThrowIfNull(payloadPath);\n\n        if (File.Exists(payloadPath))\n        {\n            return payloadPath;\n        }\n\n        using var newFile = new FileStream(payloadPath, FileMode.CreateNew);\n        newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin);\n        newFile.WriteByte(0);\n        newFile.Close();\n\n        return payloadPath;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/Program.cs",
    "content": "﻿using BenchmarkDotNet.Running;\n\nnamespace Ocelot.Benchmarks;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        var switcher = new BenchmarkSwitcher(\n            new[]\n            {\n                typeof(UrlPathToUrlPathTemplateMatcherBenchmarks),\n                typeof(AllTheThingsBenchmarks),\n                typeof(ExceptionHandlerMiddlewareBenchmarks),\n                typeof(DownstreamRouteFinderMiddlewareBenchmarks),\n                typeof(SerilogBenchmarks),\n                typeof(MsLoggerBenchmarks),\n                typeof(PayloadBenchmarks),\n                typeof(ResponseBenchmarks),\n            });\n        switcher.Run(args);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/Properties/AssemblyInfo.cs",
    "content": "﻿using System.Reflection;\r\nusing System.Runtime.InteropServices;\r\n\r\n// General Information about an assembly is controlled through the following set of attributes.\r\n// Change these attribute values to modify the information associated with an assembly.\r\n[assembly: AssemblyCompany(\"Three Mammals\")]\r\n[assembly: AssemblyCopyright(\"© 2026 Three Mammals. MIT licensed OSS.\")]\r\n[assembly: AssemblyProduct(\"Ocelot Gateway\")]\r\n[assembly: AssemblyTrademark(\"Ocelot\")]\r\n\r\n// Setting ComVisible to false makes the types in this assembly not visible to COM components.\r\n// If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type.\r\n[assembly: ComVisible(false)]\r\n\r\n// The following GUID is for the ID of the typelib if this project is exposed to COM\r\n[assembly: Guid(\"106b49e6-95f6-4a7b-b81c-96bfa74af035\")]\r\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/ResponseBenchmarks.cs",
    "content": "﻿using BenchmarkDotNet.Order;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing System.Diagnostics;\nusing System.Net.Http.Headers;\nusing System.Reflection;\nusing System.Text;\n\nnamespace Ocelot.Benchmarks;\n\n[Config(typeof(ResponseBenchmarks))]\n[Orderer(SummaryOrderPolicy.FastestToSlowest)]\npublic sealed class ResponseBenchmarks : ManualConfig, IDisposable\n{\n    private string _currentPayloadPath;\n    private bool _currentIsJson;\n    private const string BasePayload =\n        \"{\\\"_id\\\":\\\"65789c1611a3b1feb49f9e65\\\",\\\"index\\\":0,\\\"guid\\\":\\\"6622d724-c17d-4939-9c68-158bf2dc5c57\\\",\\\"isActive\\\":false,\\\"balance\\\":\\\"$1,398.26\\\",\\\"picture\\\":\\\"http://placehold.it/32x32\\\",\\\"age\\\":33,\\\"eyeColor\\\":\\\"blue\\\",\\\"name\\\":\\\"WilkersonPayne\\\",\\\"gender\\\":\\\"male\\\",\\\"company\\\":\\\"NEOCENT\\\",\\\"email\\\":\\\"wilkersonpayne@neocent.com\\\",\\\"phone\\\":\\\"+1(837)588-3248\\\",\\\"address\\\":\\\"932BatchelderStreet,Campo,Texas,1310\\\",\\\"about\\\":\\\"Dolorsuntminimnullatemporlaboretempornostrudnon.Irureconsectetursintenimestadduissunttemporquisnisi.Laboreoccaecatculpaaliquaipsumreprehenderitadofficia.Sunteuutinpariaturanimofficia.CommodosintLoremametincididuntvelitesse.Nonaliquasintdoeiusmodexercitation.Suntcommododolorcupidatatculpareprehenderitfugiatexquisamet.\\\\r\\\\n\\\",\\\"registered\\\":\\\"2021-09-06T11:54:41-02:00\\\",\\\"latitude\\\":-45.256336,\\\"longitude\\\":164.343713,\\\"tags\\\":[\\\"cillum\\\",\\\"cupidatat\\\",\\\"aliquip\\\",\\\"culpa\\\",\\\"non\\\",\\\"laboris\\\",\\\"non\\\"],\\\"friends\\\":[{\\\"id\\\":0,\\\"name\\\":\\\"MistyMorton\\\"},{\\\"id\\\":1,\\\"name\\\":\\\"AraceliAcosta\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"WalterDelaney\\\"}],\\\"greeting\\\":\\\"Hello,WilkersonPayne!Youhave1unreadmessages.\\\",\\\"favoriteFruit\\\":\\\"strawberry\\\"}\";\n    private readonly BenchmarkSteps steps = new();\n    public void Dispose() => steps.Dispose();\n\n    public ResponseBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    [GlobalSetup]\n    public void SetUp()\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = steps.GivenDefaultRoute(port);\n        var configuration = steps.GivenConfiguration(route);\n        GivenThereIsAServiceRunningOn(port, \"/\", 201);\n        steps.GivenThereIsAConfiguration(configuration);\n        steps.GivenOcelotIsRunning(host => host\n            .ConfigureKestrel((_, options) => options.Limits.MaxRequestBodySize = 2684354561)\n            .ConfigureLogging((hosting, logging) => logging.AddConfiguration(hosting.Configuration.GetSection(\"Logging\"))));\n    }\n\n    [Benchmark(Baseline = true)]\n    [ArgumentsSource(nameof(Payloads))]\n    public Task Baseline(string payLoadPath, string payloadName, bool isJson)\n    {\n        _currentPayloadPath = payLoadPath;\n        _currentIsJson = isJson;\n        return steps.WhenIGetUrlOnTheApiGateway(\"/\");\n    }\n\n    /// <summary>\n    /// Generating the payloads for the benchmarks dynamically.\n    /// </summary>\n    /// <returns>The payloads containing path, file name and a boolean indicating if the file is a json or not.</returns>\n    public static IEnumerable<object[]> Payloads()\n    {\n        var baseDirectory = GetBaseDirectory();\n        var payloadsDirectory = Path.Combine(baseDirectory, nameof(Payloads));\n\n        if (!Directory.Exists(payloadsDirectory))\n        {\n            Directory.CreateDirectory(payloadsDirectory);\n        }\n\n        // Array of sizes in kilobytes for JSON files\n        var jsonSizes = new[] { 1, 16, 32, 64, 128, 256, 512, 2 * 1024, 8 * 1024, 15 * 1024, 30 * 1024 };\n        foreach (var size in jsonSizes)\n        {\n            yield return GeneratePayload(size, payloadsDirectory, $\"{size}KBPayload.json\", true);\n        }\n\n        // Array of sizes in megabytes for DAT files\n        var datSizes = new[] { 10, 100, 1024 };\n        foreach (var size in datSizes)\n        {\n            yield return GeneratePayload(size, payloadsDirectory, $\"{size}MBPayload.dat\", false);\n        }\n    }\n\n    private static string GetBaseDirectory()\n    {\n        var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);\n        Debug.Assert(baseDirectory != null, nameof(baseDirectory) + \" != null\");\n        return baseDirectory;\n    }\n\n    private static object[] GeneratePayload(int size, string directory, string fileName, bool isJson)\n    {\n        var filePath = Path.Combine(directory, fileName);\n        var generateDummy = isJson ? (Func<int, string, string>)GenerateDummyJsonFile : GenerateDummyDatFile;\n        return new object[]\n        {\n            generateDummy(size, filePath),\n            fileName,\n            isJson,\n        };\n    }\n\n    /// <summary>\n    /// Generates a dummy payload of the given size in KB.\n    /// The payload is a JSON array of the given size.\n    /// </summary>\n    /// <param name=\"sizeInKb\">The size in KB.</param>\n    /// <param name=\"payloadPath\">The payload path.</param>\n    /// <returns>The current payload path.</returns>\n    private static string GenerateDummyJsonFile(int sizeInKb, string payloadPath)\n    {\n        ArgumentNullException.ThrowIfNull(payloadPath);\n\n        if (File.Exists(payloadPath))\n        {\n            return payloadPath;\n        }\n\n        var targetSizeInBytes = sizeInKb * 1024L;\n\n        using var fileStream = new FileStream(payloadPath, FileMode.Create, FileAccess.Write);\n        using var streamWriter = new StreamWriter(fileStream);\n\n        var byteArrayLength = Encoding.UTF8.GetBytes(BasePayload).Length;\n        var firstObject = true;\n\n        streamWriter.Write(\"[\");\n        while (fileStream.Length < targetSizeInBytes - byteArrayLength)\n        {\n            if (!firstObject)\n            {\n                streamWriter.Write(\",\");\n            }\n            else\n            {\n                firstObject = false;\n            }\n\n            streamWriter.Write(BasePayload);\n        }\n\n        streamWriter.Write(\"]\");\n\n        return payloadPath;\n    }\n\n    /// <summary>\n    /// Generates a dummy payload of the given size in MB.\n    /// Avoiding maintaining a large file in the repository.\n    /// </summary>\n    /// <param name=\"sizeInMb\">The file size in MB.</param>\n    /// <param name=\"payloadPath\">The path to the payload file.</param>\n    /// <returns>The payload file path.</returns>\n    /// <exception cref=\"ArgumentNullException\">Throwing an exception if the payload path is null.</exception>\n    private static string GenerateDummyDatFile(int sizeInMb, string payloadPath)\n    {\n        ArgumentNullException.ThrowIfNull(payloadPath);\n\n        if (File.Exists(payloadPath))\n        {\n            return payloadPath;\n        }\n\n        using var newFile = new FileStream(payloadPath, FileMode.CreateNew);\n        newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin);\n        newFile.WriteByte(0);\n        newFile.Close();\n\n        return payloadPath;\n    }\n    private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode)\n        => steps.GivenThereIsAServiceRunningOnKestrel(port, basePath,\n            options => options.Limits.MaxRequestBodySize = 2684354561,\n            async context =>\n            {\n                context.Response.StatusCode = statusCode;\n                context.Response.ContentType = string.Concat(\"application/\", _currentIsJson ? \"json\" : \"octet-stream\");\n                using var content = new StreamContent(File.OpenRead(_currentPayloadPath));\n                content.Headers.ContentType = new MediaTypeHeaderValue(string.Concat(\"application/\", _currentIsJson ? \"json\" : \"octet-stream\"));\n                await content.CopyToAsync(context.Response.Body);\n            });\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/SerilogBenchmarks.cs",
    "content": "﻿using BenchmarkDotNet.Order;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Serilog;\nusing Serilog.Core;\nusing System.Net;\n\nnamespace Ocelot.Benchmarks;\n\n[Config(typeof(SerilogBenchmarks))]\n[Orderer(SummaryOrderPolicy.FastestToSlowest)]\npublic sealed class SerilogBenchmarks : ManualConfig, IDisposable\n{\n    private Logger _logger;\n    private readonly BenchmarkSteps steps = new();\n    public void Dispose() => steps.Dispose();\n\n    public SerilogBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    private Task SendRequest()\n    {\n        return steps.WhenIGetUrlOnTheApiGateway(\"/\");\n    }\n\n    [Benchmark(Baseline = true)]\n    public async Task LogLevelCritical() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelCritical))]\n    public void SetUpCritical() => OcelotFactory(LogLevel.Critical);\n\n    [Benchmark]\n    public async Task LogLevelError() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelError))]\n    public void SetupError() => OcelotFactory(LogLevel.Error);\n\n    [Benchmark]\n    public async Task LogLevelWarning() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelWarning))]\n    public void SetUpWarning() => OcelotFactory(LogLevel.Warning);\n\n    [Benchmark]\n    public async Task LogLevelInformation() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelInformation))]\n    public void SetUpInformation() => OcelotFactory(LogLevel.Information);\n\n    [Benchmark]\n    public async Task LogLevelTrace() => await SendRequest();\n\n    [GlobalSetup(Target = nameof(LogLevelTrace))]\n    public void SetUpTrace() => OcelotFactory(LogLevel.Trace);\n\n    [GlobalCleanup(Targets = new[]\n    {\n        nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation),\n        nameof(LogLevelTrace),\n    })]\n    public void OcelotCleanup()\n    {\n        steps.Dispose();\n    }\n\n    [GlobalCleanup]\n    public void Cleanup()\n    {\n        Dispose();\n    }\n\n    private void GivenOcelotIsRunning(LogLevel minLogLevel)\n    {\n        _logger = minLogLevel switch\n        {\n            LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            LogLevel.None => new LoggerConfiguration()\n                .WriteTo.File(\n                    $\"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log\")\n                .CreateLogger(),\n            _ => throw new ArgumentOutOfRangeException(nameof(minLogLevel), minLogLevel, null),\n        };\n        steps.GivenOcelotIsRunning(\n            async app => await app\n                .Use(async (context, next) =>\n                {\n                    var loggerFactory = context.RequestServices.GetService<IOcelotLoggerFactory>();\n                    var ocelotLogger = loggerFactory.CreateLogger<SerilogBenchmarks>();\n                    ocelotLogger.LogDebug(() => $\"DEBUG: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogTrace(() => $\"TRACE: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogInformation(() => $\"INFORMATION: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogWarning(() => $\"WARNING: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\");\n                    ocelotLogger.LogError(() => $\"ERROR: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n                    ocelotLogger.LogCritical(() => $\"CRITICAL: {nameof(ocelotLogger)},  {nameof(loggerFactory)}\",\n                        new Exception(\"test\"));\n                    await next.Invoke();\n                })\n                .UseOcelot(), // Action<IApplicationBuilder> configureApp\n            host => host.ConfigureLogging(\n                (c, l) => l.ClearProviders().SetMinimumLevel(minLogLevel).AddSerilog(_logger)) // Action<IWebHostBuilder> postConfigureHost\n        );\n    }\n\n    private void OcelotFactory(LogLevel minLogLevel)\n    {\n        var port = PortFinder.GetRandomPort();\n        var route = steps.GivenDefaultRoute(port);\n        var configuration = steps.GivenConfiguration(route);\n        steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(SerilogBenchmarks));\n        steps.GivenThereIsAConfiguration(configuration);\n        GivenOcelotIsRunning(minLogLevel);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs",
    "content": "using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.Benchmarks;\n\n[Config(typeof(UrlPathToUrlPathTemplateMatcherBenchmarks))]\npublic class UrlPathToUrlPathTemplateMatcherBenchmarks : ManualConfig\n{\n    private RegExUrlMatcher _urlPathMatcher;\n    private UpstreamPathTemplate _pathTemplate;\n    private string _downstreamUrlPath;\n\n    public UrlPathToUrlPathTemplateMatcherBenchmarks()\n    {\n        AddColumn(StatisticColumn.AllStatistics);\n        AddDiagnoser(MemoryDiagnoser.Default);\n        AddValidator(BaselineValidator.FailOnError);\n    }\n\n    [GlobalSetup]\n    public void SetUp()\n    {\n        _urlPathMatcher = new RegExUrlMatcher();\n        _pathTemplate = new UpstreamPathTemplate(\"api/product/products/{productId}/variants/\", 0, false, null);\n        _downstreamUrlPath = \"api/product/products/1/variants/?soldout=false\";\n    }\n\n    [Benchmark(Baseline = true)]\n    public void Baseline()\n    {\n        _urlPathMatcher.Match(_downstreamUrlPath, null, _pathTemplate);\n    }\n\n    // * Summary *\n\n    // BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0]\n    // Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores\n    // .NET Core SDK=2.1.4\n    //   [Host]     : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT\n    //   DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT\n    //      Method |     Mean |     Error |    StdDev |    StdErr |      Min |       Q1 |   Median |       Q3 |      Max |      Op/s |\n    // ----------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|----------:|\n    //  Benchmark1 | 3.133 us | 0.0492 us | 0.0460 us | 0.0119 us | 3.082 us | 3.100 us | 3.122 us | 3.168 us | 3.233 us | 319,161.9 |\n}\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using BenchmarkDotNet.Attributes;\nglobal using BenchmarkDotNet.Columns;\nglobal using BenchmarkDotNet.Configs;\nglobal using BenchmarkDotNet.Diagnosers;\nglobal using BenchmarkDotNet.Validators;\nglobal using Ocelot;\nglobal using Ocelot.Testing;\n"
  },
  {
    "path": "test/Ocelot.Benchmarks/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"BenchmarkDotNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[0.15.8, )\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==\",\n        \"dependencies\": {\n          \"BenchmarkDotNet.Annotations\": \"0.15.8\",\n          \"CommandLineParser\": \"2.9.1\",\n          \"Gee.External.Capstone\": \"2.3.0\",\n          \"Iced\": \"1.21.0\",\n          \"Microsoft.CodeAnalysis.CSharp\": \"4.14.0\",\n          \"Microsoft.Diagnostics.Runtime\": \"3.1.512801\",\n          \"Microsoft.Diagnostics.Tracing.TraceEvent\": \"3.1.21\",\n          \"Microsoft.DotNet.PlatformAbstractions\": \"3.1.6\",\n          \"Perfolizer\": \"[0.6.1]\",\n          \"System.Management\": \"9.0.5\"\n        }\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"BenchmarkDotNet.Annotations\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"CommandLineParser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.9.1\",\n        \"contentHash\": \"OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"Iced\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.21.0\",\n        \"contentHash\": \"dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==\"\n      },\n      \"Microsoft.CodeAnalysis.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.11.0\",\n        \"contentHash\": \"v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==\"\n      },\n      \"Microsoft.CodeAnalysis.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\"\n        }\n      },\n      \"Microsoft.CodeAnalysis.CSharp\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\",\n          \"Microsoft.CodeAnalysis.Common\": \"[4.14.0]\"\n        }\n      },\n      \"Microsoft.Diagnostics.NETCore.Client\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.2.510501\",\n        \"contentHash\": \"juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"6.0.0\"\n        }\n      },\n      \"Microsoft.Diagnostics.Runtime\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.512801\",\n        \"contentHash\": \"0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.410101\"\n        }\n      },\n      \"Microsoft.Diagnostics.Tracing.TraceEvent\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.21\",\n        \"contentHash\": \"/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.510501\",\n          \"System.Reflection.TypeExtensions\": \"4.7.0\"\n        }\n      },\n      \"Microsoft.DotNet.PlatformAbstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.6\",\n        \"contentHash\": \"jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==\"\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"8.0.1\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Perfolizer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.6.1\",\n        \"contentHash\": \"CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==\",\n        \"dependencies\": {\n          \"Pragmastat\": \"3.2.4\"\n        }\n      },\n      \"Pragmastat\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.4\",\n        \"contentHash\": \"I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==\"\n      },\n      \"Serilog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.0.1\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Reflection.TypeExtensions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.7.0\",\n        \"contentHash\": \"VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[10.0.5, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      }\n    },\n    \"net10.0/win-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      }\n    },\n    \"net8.0\": {\n      \"BenchmarkDotNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[0.15.8, )\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==\",\n        \"dependencies\": {\n          \"BenchmarkDotNet.Annotations\": \"0.15.8\",\n          \"CommandLineParser\": \"2.9.1\",\n          \"Gee.External.Capstone\": \"2.3.0\",\n          \"Iced\": \"1.21.0\",\n          \"Microsoft.CodeAnalysis.CSharp\": \"4.14.0\",\n          \"Microsoft.Diagnostics.Runtime\": \"3.1.512801\",\n          \"Microsoft.Diagnostics.Tracing.TraceEvent\": \"3.1.21\",\n          \"Microsoft.DotNet.PlatformAbstractions\": \"3.1.6\",\n          \"Perfolizer\": \"[0.6.1]\",\n          \"System.Management\": \"9.0.5\"\n        }\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"BenchmarkDotNet.Annotations\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"CommandLineParser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.9.1\",\n        \"contentHash\": \"OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"Iced\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.21.0\",\n        \"contentHash\": \"dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.CodeAnalysis.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.11.0\",\n        \"contentHash\": \"v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==\"\n      },\n      \"Microsoft.CodeAnalysis.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\",\n          \"System.Collections.Immutable\": \"9.0.0\",\n          \"System.Reflection.Metadata\": \"9.0.0\"\n        }\n      },\n      \"Microsoft.CodeAnalysis.CSharp\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\",\n          \"Microsoft.CodeAnalysis.Common\": \"[4.14.0]\",\n          \"System.Collections.Immutable\": \"9.0.0\",\n          \"System.Reflection.Metadata\": \"9.0.0\"\n        }\n      },\n      \"Microsoft.Diagnostics.NETCore.Client\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.2.510501\",\n        \"contentHash\": \"juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"6.0.0\"\n        }\n      },\n      \"Microsoft.Diagnostics.Runtime\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.512801\",\n        \"contentHash\": \"0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.410101\"\n        }\n      },\n      \"Microsoft.Diagnostics.Tracing.TraceEvent\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.21\",\n        \"contentHash\": \"/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.510501\"\n        }\n      },\n      \"Microsoft.DotNet.PlatformAbstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.6\",\n        \"contentHash\": \"jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==\"\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\",\n        \"dependencies\": {\n          \"System.Text.Encodings.Web\": \"10.0.0\",\n          \"System.Text.Json\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"33eTIA2uO/L9utJjZWbKsMSVsQf7F8vtd6q5mQX7ZJzNvCpci5fleD6AeANGlbbb7WX7XKxq9+Dkb5e3GNDrmQ==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"cloLGeZolXbCJhJBc5OC05uhrdhdPL6MWHuVUnkkUvPDeK7HkwThBaLZ1XjBQVk9YhxXE2OvHXnKi0PLleXxDg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"YCxBt2EeJP8fcXk9desChkWI+0vFqFLvBwrz5hBMsoh0KJE6BC66DnzkdzkJNqMltLromc52dkdT206jJ38cTw==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"7.1.2\",\n          \"System.IdentityModel.Tokens.Jwt\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"oICJMqr3aNEDZOwnH5SK49bR6Z4aX0zEAnOLuhloumOSuqnNq+GWBdQyrgILnlcT5xj09xKCP/7Y7gJYB+ls/g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Perfolizer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.6.1\",\n        \"contentHash\": \"CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==\",\n        \"dependencies\": {\n          \"Pragmastat\": \"3.2.4\"\n        }\n      },\n      \"Pragmastat\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.4\",\n        \"contentHash\": \"I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==\"\n      },\n      \"Serilog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==\"\n      },\n      \"System.Collections.Immutable\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.0\",\n        \"contentHash\": \"QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"0KdBK+h7G13PuOSC2R/DalAoFMvdYMznvGRuICtkdcUMXgl/gYXsG6z4yUvTxHSMACorWgHCU1Faq0KUHU6yAQ==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"Thhbe1peAmtSBFaV/ohtykXiZSOkx59Da44hvtWfIMFofDA3M3LaVyjstACf2rKGn4dEDR2cUpRAZ0Xs/zB+7Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Reflection.Metadata\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.0\",\n        \"contentHash\": \"ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==\",\n        \"dependencies\": {\n          \"System.Collections.Immutable\": \"9.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[8.0.25, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net8.0/win-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0\": {\n      \"BenchmarkDotNet\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[0.15.8, )\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==\",\n        \"dependencies\": {\n          \"BenchmarkDotNet.Annotations\": \"0.15.8\",\n          \"CommandLineParser\": \"2.9.1\",\n          \"Gee.External.Capstone\": \"2.3.0\",\n          \"Iced\": \"1.21.0\",\n          \"Microsoft.CodeAnalysis.CSharp\": \"4.14.0\",\n          \"Microsoft.Diagnostics.Runtime\": \"3.1.512801\",\n          \"Microsoft.Diagnostics.Tracing.TraceEvent\": \"3.1.21\",\n          \"Microsoft.DotNet.PlatformAbstractions\": \"3.1.6\",\n          \"Perfolizer\": \"[0.6.1]\",\n          \"System.Management\": \"9.0.5\"\n        }\n      },\n      \"Serilog.AspNetCore\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.0, )\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Hosting\": \"10.0.0\",\n          \"Serilog.Formatting.Compact\": \"3.0.0\",\n          \"Serilog.Settings.Configuration\": \"10.0.0\",\n          \"Serilog.Sinks.Console\": \"6.1.1\",\n          \"Serilog.Sinks.Debug\": \"3.0.0\",\n          \"Serilog.Sinks.File\": \"7.0.0\"\n        }\n      },\n      \"BenchmarkDotNet.Annotations\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.15.8\",\n        \"contentHash\": \"hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"CommandLineParser\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.9.1\",\n        \"contentHash\": \"OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"Iced\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.21.0\",\n        \"contentHash\": \"dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==\"\n      },\n      \"Microsoft.CodeAnalysis.Analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.11.0\",\n        \"contentHash\": \"v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==\"\n      },\n      \"Microsoft.CodeAnalysis.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\"\n        }\n      },\n      \"Microsoft.CodeAnalysis.CSharp\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.14.0\",\n        \"contentHash\": \"568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==\",\n        \"dependencies\": {\n          \"Microsoft.CodeAnalysis.Analyzers\": \"3.11.0\",\n          \"Microsoft.CodeAnalysis.Common\": \"[4.14.0]\"\n        }\n      },\n      \"Microsoft.Diagnostics.NETCore.Client\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.2.510501\",\n        \"contentHash\": \"juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"6.0.0\"\n        }\n      },\n      \"Microsoft.Diagnostics.Runtime\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.512801\",\n        \"contentHash\": \"0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.410101\"\n        }\n      },\n      \"Microsoft.Diagnostics.Tracing.TraceEvent\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.21\",\n        \"contentHash\": \"/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==\",\n        \"dependencies\": {\n          \"Microsoft.Diagnostics.NETCore.Client\": \"0.2.510501\"\n        }\n      },\n      \"Microsoft.DotNet.PlatformAbstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.6\",\n        \"contentHash\": \"jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==\"\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==\"\n      },\n      \"Microsoft.Extensions.DependencyModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==\",\n        \"dependencies\": {\n          \"System.Text.Encodings.Web\": \"10.0.0\",\n          \"System.Text.Json\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"8.0.1\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Perfolizer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"0.6.1\",\n        \"contentHash\": \"CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==\",\n        \"dependencies\": {\n          \"Pragmastat\": \"3.2.4\"\n        }\n      },\n      \"Pragmastat\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.4\",\n        \"contentHash\": \"I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==\"\n      },\n      \"Serilog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==\"\n      },\n      \"Serilog.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\",\n          \"Serilog.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"Serilog.Extensions.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Serilog.Formatting.Compact\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Settings.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyModel\": \"10.0.0\",\n          \"Serilog\": \"4.3.0\"\n        }\n      },\n      \"Serilog.Sinks.Console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.1.1\",\n        \"contentHash\": \"8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.Debug\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.0.0\",\n        \"contentHash\": \"4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.0.0\"\n        }\n      },\n      \"Serilog.Sinks.File\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0\",\n        \"contentHash\": \"fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==\",\n        \"dependencies\": {\n          \"Serilog\": \"4.2.0\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"0KdBK+h7G13PuOSC2R/DalAoFMvdYMznvGRuICtkdcUMXgl/gYXsG6z4yUvTxHSMACorWgHCU1Faq0KUHU6yAQ==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.0.1\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[9.0.14, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0/win-x64\": {\n      \"Gee.External.Capstone\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.3.0\",\n        \"contentHash\": \"2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.5\",\n        \"contentHash\": \"n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"9.0.5\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "test/Ocelot.ManualTest/Actions/Basic.cs",
    "content": "﻿using Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\n\nnamespace Ocelot.ManualTest.Actions;\n\npublic class Basic\n{\n    internal static async Task RunAsync(string[] args)\n    {\n        Console.WriteLine(\"Starting Ocelot... \");\n        var builder = WebApplication.CreateBuilder(args);\n        builder.Configuration\n            .SetBasePath(builder.Environment.ContentRootPath)\n            .AddOcelot();\n\n        builder.Services\n\n            //.AddAuthentication()\n            //.AddJwtBearer(\"TestKey\", x =>\n            //{\n            //    x.Authority = \"test\";\n            //    x.Audience = \"test\";\n            //});\n            //.AddSingleton<QosDelegatingHandlerDelegate>((x, t, f) => new FakeHandler(f))\n            .AddOcelot(builder.Configuration);\n\n            //.AddDelegatingHandler<FakeHandler>(true);\n            //.AddAdministration(\"/administration\", \"secret\");\n        builder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\"));\n        if (builder.Environment.IsDevelopment())\n        {\n            builder.Logging.AddConsole();\n        }\n\n        var app = builder.Build();\n\n        await app.UseOcelot();\n\n        //await app.UseOcelot(pipeline =>\n        //{\n        //    pipeline.PreAuthenticationMiddleware = MetadataMiddleware.Invoke;\n        //});\n        await app.RunAsync(); // Ocelot will exit from this method when pressing Ctrl+C\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Actions/ManualTests.cs",
    "content": "﻿namespace Ocelot.ManualTest.Actions;\n\npublic class ManualTests\n{\n    internal static void Run(string[] args)\n    {\n        Console.WriteLine(\"No tests to perform!\");\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/DelegatingHandlers/FakeHandler.cs",
    "content": "﻿using Ocelot.Logging;\n\nnamespace Ocelot.ManualTest.DelegatingHandlers;\n\npublic class FakeHandler : DelegatingHandler\n{\n    private readonly IOcelotLogger _logger;\n\n    public FakeHandler(IOcelotLoggerFactory factory)\n    {\n        _logger = factory.CreateLogger<FakeHandler>();\n    }\n\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        _logger.LogDebug(() => $\"{nameof(request.RequestUri)}: {request.RequestUri}\");\n\n        // Do stuff and optionally call the base handler..\n        return base.SendAsync(request, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Dockerfile",
    "content": "#This is the base image used for any ran images\nFROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base\nWORKDIR /app\nEXPOSE 80\n\n#This image is used to build the source for the runnable app\n#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples:\n#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx\n#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj\nFROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder\nWORKDIR /build\n#First we add only the project files so that we can cache nuget packages with dotnet restore\nCOPY Ocelot.sln Ocelot.sln\nCOPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj\nCOPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj\nCOPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj\nCOPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj\nCOPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj\nCOPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj\nCOPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj\nCOPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj\nCOPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj\nCOPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj\nCOPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj\nCOPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj\n\nRUN dotnet restore\n#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point\nCOPY codeanalysis.ruleset codeanalysis.ruleset\nCOPY src src\nCOPY test test\nARG build_configuration=Debug\nRUN dotnet build --no-restore -c ${build_configuration}\nENTRYPOINT [\"dotnet\"]\n\n#This is just for holding the published manual tests...\nFROM builder AS manual-test-publish\nARG build_configuration=Debug\nRUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest\n\n#Run manual tests! This is the default run option.\n#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test\nFROM base AS manual-test\nENV ASPNETCORE_ENVIRONMENT=Development\nCOPY --from=manual-test-publish /app .\nENTRYPOINT [\"dotnet\", \"Ocelot.ManualTest.dll\"]\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Middlewares/MetadataMiddleware.cs",
    "content": "﻿using Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Text.Json;\n\nnamespace Ocelot.ManualTest.Middlewares;\n\npublic class MetadataMiddleware\n{\n    public static Task Invoke(HttpContext context, Func<Task> next)\n    {\n        var logger = GetLogger(context);\n        var downstreamRoute = context.Items.DownstreamRoute();\n\n        if (downstreamRoute?.MetadataOptions?.Metadata is { } metadata)\n        {\n            logger.LogInformation(() =>\n            {\n                var metadataInJson = JsonSerializer.Serialize(metadata, JsonSerializerOptions.Web);\n                var message = $\"{nameof(MetadataMiddleware)} found some metadata: {metadataInJson}\";\n                return message;\n            });\n        }\n\n        return next();\n    }\n\n    private static IOcelotLogger GetLogger(HttpContext context)\n    {\n        var loggerFactory = context.RequestServices.GetRequiredService<IOcelotLoggerFactory>();\n        return loggerFactory.CreateLogger<MetadataMiddleware>();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Ocelot.ManualTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <PreserveCompilationContext>true</PreserveCompilationContext>\n    <OutputType>Exe</OutputType>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <NoWarn>$(NoWarn);CS0618;CS1591</NoWarn>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot/tree/main/test/Ocelot.ManualTest</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"..\\..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Update=\"Views;Areas\\**\\Views\">\n      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n    </None>\n  </ItemGroup>\n  <ItemGroup>\n    <None Update=\"ocelot.json\">\n      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n    </None>\n  </ItemGroup>\n  <ItemGroup>\n    <None Update=\"ocelot.json;appsettings.json;mycert.pfx\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\testing\\Ocelot.Testing.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.FileExtensions\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Console\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Debug\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" Version=\"10.0.5\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Ocelot.postman_collection.json",
    "content": "{\r\n  \"id\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\r\n  \"name\": \"Ocelot\",\r\n  \"description\": \"\",\r\n  \"order\": [\r\n    \"a1c95935-ed18-d5dc-bcb8-a3db8ba1934f\",\r\n    \"ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2\",\r\n    \"c4494401-3985-a5bf-71fb-6e4171384ac6\",\r\n    \"09af8dda-a9cb-20d2-5ee3-0a3023773a1a\",\r\n    \"e8825dc3-4137-99a7-0000-ef5786610dc3\",\r\n    \"fddfc4fa-5114-69e3-4744-203ed71a526b\",\r\n    \"c45d30d7-d9c4-fa05-8110-d6e769bb6ff9\",\r\n    \"4684c2fa-f38c-c193-5f55-bf563a1978c6\",\r\n    \"37bfa9f1-fe29-6a68-e558-66d125d2c96f\",\r\n    \"5f308240-79e3-cf74-7a6b-fe462f0d54f1\",\r\n    \"178f16da-c61b-c881-1c33-9d64a56851a4\"\r\n  ],\r\n  \"folders\": [],\r\n  \"folders_order\": [],\r\n  \"timestamp\": 0,\r\n  \"owner\": \"212120\",\r\n  \"public\": false,\r\n  \"requests\": [\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"09af8dda-a9cb-20d2-5ee3-0a3023773a1a\",\r\n      \"name\": \"GET http://localhost:5000/comments?postId=1\",\r\n      \"dataMode\": \"params\",\r\n      \"data\": null,\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"GET\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/comments?postId=1\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"id\": \"178f16da-c61b-c881-1c33-9d64a56851a4\",\r\n      \"headers\": \"Authorization: Bearer {{AccessToken}}\\n\",\r\n      \"headerData\": [\r\n        {\r\n          \"key\": \"Authorization\",\r\n          \"value\": \"Bearer {{AccessToken}}\",\r\n          \"enabled\": true,\r\n          \"description\": \"\"\r\n        }\r\n      ],\r\n      \"url\": \"http://localhost:5000/administration/configuration\",\r\n      \"folder\": null,\r\n      \"queryParams\": [],\r\n      \"preRequestScript\": null,\r\n      \"pathVariables\": {},\r\n      \"pathVariableData\": [],\r\n      \"method\": \"GET\",\r\n      \"data\": null,\r\n      \"dataMode\": \"params\",\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"time\": 1508849878025,\r\n      \"name\": \"GET http://localhost:5000/admin/configuration\",\r\n      \"description\": \"\",\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\r\n      \"responses\": [],\r\n      \"isFromCollection\": true,\r\n      \"collectionRequestId\": \"178f16da-c61b-c881-1c33-9d64a56851a4\",\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": null\r\n    },\r\n    {\r\n      \"id\": \"37bfa9f1-fe29-6a68-e558-66d125d2c96f\",\r\n      \"headers\": \"\",\r\n      \"headerData\": [],\r\n      \"url\": \"http://localhost:5000/administration/connect/token\",\r\n      \"folder\": null,\r\n      \"queryParams\": [],\r\n      \"preRequestScript\": null,\r\n      \"pathVariables\": {},\r\n      \"pathVariableData\": [],\r\n      \"method\": \"POST\",\r\n      \"data\": [\r\n        {\r\n          \"key\": \"client_id\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"client_secret\",\r\n          \"value\": \"secret\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"scope\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"username\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"password\",\r\n          \"value\": \"secret\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"grant_type\",\r\n          \"value\": \"password\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        }\r\n      ],\r\n      \"dataMode\": \"params\",\r\n      \"tests\": \"var jsonData = JSON.parse(responseBody);\\npostman.setGlobalVariable(\\\"AccessToken\\\", jsonData.access_token);\\npostman.setGlobalVariable(\\\"RefreshToken\\\", jsonData.refresh_token);\",\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"time\": 1506359585080,\r\n      \"name\": \"POST http://localhost:5000/admin/connect/token copy\",\r\n      \"description\": \"\",\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\r\n      \"responses\": [],\r\n      \"isFromCollection\": true,\r\n      \"collectionRequestId\": \"37bfa9f1-fe29-6a68-e558-66d125d2c96f\",\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": null\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"4684c2fa-f38c-c193-5f55-bf563a1978c6\",\r\n      \"name\": \"DELETE http://localhost:5000/posts/1\",\r\n      \"dataMode\": \"params\",\r\n      \"data\": null,\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"DELETE\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts/1\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"id\": \"5f308240-79e3-cf74-7a6b-fe462f0d54f1\",\r\n      \"headers\": \"Authorization: Bearer {{AccessToken}}\\n\",\r\n      \"headerData\": [\r\n        {\r\n          \"key\": \"Authorization\",\r\n          \"value\": \"Bearer {{AccessToken}}\",\r\n          \"description\": \"\",\r\n          \"enabled\": true\r\n        }\r\n      ],\r\n      \"url\": \"http://localhost:5000/administration/.well-known/openid-configuration\",\r\n      \"folder\": null,\r\n      \"queryParams\": [],\r\n      \"preRequestScript\": null,\r\n      \"pathVariables\": {},\r\n      \"pathVariableData\": [],\r\n      \"method\": \"GET\",\r\n      \"data\": null,\r\n      \"dataMode\": \"params\",\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": {},\r\n      \"time\": 1508849923518,\r\n      \"name\": \"GET http://localhost:5000/admin/.well-known/openid-configuration\",\r\n      \"description\": \"\",\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\",\r\n      \"responses\": []\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"a1c95935-ed18-d5dc-bcb8-a3db8ba1934f\",\r\n      \"name\": \"GET http://localhost:5000/posts\",\r\n      \"dataMode\": \"params\",\r\n      \"data\": [\r\n        {\r\n          \"key\": \"client_id\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"client_secret\",\r\n          \"value\": \"secret\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"scope\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"username\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"password\",\r\n          \"value\": \"admin\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        },\r\n        {\r\n          \"key\": \"grant_type\",\r\n          \"value\": \"password\",\r\n          \"type\": \"text\",\r\n          \"enabled\": true\r\n        }\r\n      ],\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"POST\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/admin/configuration\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"c4494401-3985-a5bf-71fb-6e4171384ac6\",\r\n      \"name\": \"GET http://localhost:5000/posts/1/comments\",\r\n      \"dataMode\": \"params\",\r\n      \"data\": null,\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"GET\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts/1/comments\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"c45d30d7-d9c4-fa05-8110-d6e769bb6ff9\",\r\n      \"name\": \"PATCH http://localhost:5000/posts/1\",\r\n      \"dataMode\": \"raw\",\r\n      \"data\": [],\r\n      \"rawModeData\": \"{\\n  \\\"title\\\": \\\"gfdgsgsdgsdfgsdfgdfg\\\",\\n}\",\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"PATCH\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts/1\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"e8825dc3-4137-99a7-0000-ef5786610dc3\",\r\n      \"name\": \"POST http://localhost:5000/posts/1\",\r\n      \"dataMode\": \"raw\",\r\n      \"data\": [],\r\n      \"rawModeData\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"title\\\": \\\"test\\\",\\n  \\\"body\\\": \\\"test\\\"\\n}\",\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"POST\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2\",\r\n      \"name\": \"GET http://localhost:5000/posts/1\",\r\n      \"dataMode\": \"params\",\r\n      \"data\": null,\r\n      \"rawModeData\": null,\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"GET\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts/1\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    },\r\n    {\r\n      \"folder\": null,\r\n      \"id\": \"fddfc4fa-5114-69e3-4744-203ed71a526b\",\r\n      \"name\": \"PUT http://localhost:5000/posts/1\",\r\n      \"dataMode\": \"raw\",\r\n      \"data\": [],\r\n      \"rawModeData\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"title\\\": \\\"test\\\",\\n  \\\"body\\\": \\\"test\\\"\\n}\",\r\n      \"descriptionFormat\": \"html\",\r\n      \"description\": \"\",\r\n      \"headers\": \"\",\r\n      \"method\": \"PUT\",\r\n      \"pathVariables\": {},\r\n      \"url\": \"http://localhost:5000/posts/1\",\r\n      \"preRequestScript\": null,\r\n      \"tests\": null,\r\n      \"currentHelper\": \"normal\",\r\n      \"helperAttributes\": \"{}\",\r\n      \"queryParams\": [],\r\n      \"headerData\": [],\r\n      \"pathVariableData\": [],\r\n      \"responses\": [],\r\n      \"collectionId\": \"4dbde9fe-89f5-be35-bb9f-d3b438e16375\"\r\n    }\r\n  ]\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Program.cs",
    "content": "﻿using Ocelot.ManualTest.Actions;\nusing System.Reflection;\n\nvar nl = Environment.NewLine;\nvar programName = Assembly.GetExecutingAssembly().GetName()?.Name?.Replace(\".\", \" \") ?? \"?\";\ndo\n{\n    Console.Clear();\n    Console.WriteLine($\"{nl}Welcome to {programName} app!\");\n    Console.Write(@\"What are you going to do?\n  1. Run Ocelot with basic setup (default)\n  2. Run Ocelot manual tests\nSo, press 1 or 2 > \");\n    ConsoleKeyInfo info = Console.ReadKey(true);\n    if (info.Key == ConsoleKey.D2)\n    {\n        Console.WriteLine((char)info.Key);\n        ManualTests.Run(args);\n    }\n    else\n    {\n        Console.WriteLine($\"{(char)info.Key} -> 1 (default)\");\n        await Basic.RunAsync(args);\n    }\n}\nwhile (!Quit());\n\nbool Quit()\n{\n    Console.WriteLine(nl + \"Enter Ctrl+Q to Quit, Ctrl+E to Exit, Ctrl+L to Clear the log\");\n    Console.Write(\"Or press any key to restart... \");\n    ConsoleKeyInfo info = Console.ReadKey(true);\n    if (info.Modifiers == ConsoleModifiers.Control)\n    {\n        if (info.Key == ConsoleKey.Q)\n        {\n            Console.WriteLine(\"Quitting...\");\n            Environment.ExitCode = 0;\n            return true;\n        }\n        else if (info.Key == ConsoleKey.E)\n        {\n            Console.WriteLine(\"Exitting...\");\n            Environment.Exit(1);\n        }\n        else if (info.Key == ConsoleKey.L)\n        {\n            Console.WriteLine();\n            Console.Clear();\n        }\n    }\n\n    Console.WriteLine();\n    return false;\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Properties/launchSettings.json",
    "content": "{\r\n  \"iisSettings\": {\r\n    \"windowsAuthentication\": false,\r\n    \"anonymousAuthentication\": true,\r\n    \"iisExpress\": {\r\n      \"applicationUrl\": \"http://localhost:24620/\",\r\n      \"sslPort\": 0\r\n    }\r\n  },\r\n  \"profiles\": {\r\n    \"IIS Express\": {\r\n      \"commandName\": \"IISExpress\",\r\n      \"launchBrowser\": true,\r\n      \"environmentVariables\": {\r\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\r\n      }\r\n    },\r\n    \"Ocelot.ManualTest\": {\r\n      \"commandName\": \"Project\",\r\n      \"launchUrl\": \"http://localhost:5000\",\r\n      \"environmentVariables\": {\r\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\r\n      }\r\n    }\r\n  }\r\n}"
  },
  {
    "path": "test/Ocelot.ManualTest/Tests/Bug0930.html",
    "content": "﻿<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Bug 930 Test</title>\n  <link rel=\"icon\" type=\"image/png\" href=\"ocelot_icon.png\">\n  <style>\n    .title-container {\n      display: flex;\n      align-items: center; /* Aligns items vertically */\n    }\n\n    .logo {\n      width: 50px; /* Adjust size */\n      height: auto;\n      margin-right: 10px; /* Spacing */\n    }\n  </style>\n</head>\n<!--<body style=\"background-color: darkslategray; color: azure;\">-->\n<body>\n  <div class=\"title-container\">\n    <img alt=\"Ocelot Icon\" src=\"ocelot_icon.png\" class=\"logo\">\n    <h1>Bug <a href=\"https://github.com/ThreeMammals/Ocelot/issues/930\">930</a> Test</h1>\n  </div>\n  <hr />\n  <h2>WebSocket Connection</h2>\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx\" checked> Direct &rarr; ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx\n    </label>\n  </div>\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"ws://localhost:5000/bug930/ws1\"> Ocelot &rarr; ws://localhost:5000/bug930/ws1\n    </label>\n  </div>\n  <br />\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"wss://echo.websocket.org\"> Direct &rarr; wss://echo.websocket.org\n    </label>\n  </div>\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"ws://localhost:5000/bug930/ws2\"> Ocelot &rarr; ws://localhost:5000/bug930/ws2\n    </label>\n  </div>\n  <br />\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"wss://ws.postman-echo.com/raw\"> Direct &rarr; wss://ws.postman-echo.com/raw\n    </label>\n  </div>\n  <div>\n    <label>\n      <input type=\"radio\" name=\"connection\" value=\"ws://localhost:5000/bug930/ws3\"> Ocelot &rarr; ws://localhost:5000/bug930/ws3\n    </label>\n  </div>\n  <br />\n  <br />\n  <label>\n    <button id=\"connectBtn\">Connect</button>\n    <span id=\"status\">Not connected</span>\n  </label>\n\n  <script>\n    let socket;\n    let interval;\n    let url = \"ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx\"; // \"ws://localhost:5000/bug930\";\n    const button = document.getElementById(\"connectBtn\");\n    const status = document.getElementById(\"status\");\n\n    function getSelectedConnection() {\n      return document.querySelector('input[name=\"connection\"]:checked').value;\n    }\n    function generateRandomString() {\n      return Math.random().toString(36).substring(2, 10);\n    }\n    function sendMessage(msg) {\n      if (!msg || msg.trim() === \"\") {\n        msg = generateRandomString();\n      }\n      socket.send(msg);\n      console.log(\"Sent:\", msg);\n    }\n\n    button.addEventListener(\"click\", function () {\n      if (!socket || socket.readyState === WebSocket.CLOSED) {\n        const connectionString = getSelectedConnection();\n        console.log(\"Connecting to:\", connectionString);\n        socket = new WebSocket(connectionString); // open connection\n\n        socket.onopen = () => {\n          status.textContent = \"Connected\";\n          button.textContent = \"Disconnect\";\n          console.log(\"WebSocket connected to\", connectionString);\n          sendMessage(\"Hello!\");\n\n          // Start sending a random string every 2 seconds\n          interval = setInterval(sendMessage, 2000);\n        };\n\n        socket.onmessage = (event) => {\n          console.log(\"Received:\", event.data);\n        };\n\n        socket.onerror = (error) => {\n          console.error(\"WebSocket error:\", error);\n        };\n\n        socket.onclose = () => {\n          status.textContent = \"Disconnected\";\n          button.textContent = \"Connect\";\n          console.log(\"WebSocket closed\");\n          clearInterval(interval); // stop sending messages\n        };\n      } else {\n        socket.close(); // close connection\n      }\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "test/Ocelot.ManualTest/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk.Web namespaces\nglobal using System.Net.Http.Json;\nglobal using Microsoft.AspNetCore.Builder;\nglobal using Microsoft.AspNetCore.Hosting;\nglobal using Microsoft.AspNetCore.Http;\nglobal using Microsoft.AspNetCore.Routing;\nglobal using Microsoft.Extensions.Configuration;\nglobal using Microsoft.Extensions.DependencyInjection;\nglobal using Microsoft.Extensions.Hosting;\nglobal using Microsoft.Extensions.Logging;\n\n// Project extra global namespaces\nglobal using Ocelot;\nglobal using Ocelot.Testing;\nglobal using System;\n"
  },
  {
    "path": "test/Ocelot.ManualTest/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\",\n      \"System\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/appsettings.json",
    "content": "{\r\n  \"Logging\": {\r\n    \"LogLevel\": {\r\n      \"Default\": \"Warning\"\r\n    }\r\n  },\r\n  \"AllowedHosts\": \"*\"\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.ManualTest/docker-compose.yaml",
    "content": "version: \"3.4\"\nservices:\n\n  tests:\n    build:\n      context: .\n      target: builder\n    volumes:\n      - type: bind\n        source: .\n        target: /results\n    command: test --logger:trx -r /results\n\n  benchmarks:\n    build:\n      context: .\n      target: builder\n      args:\n        build_configuration: Release\n    command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4\n\n  manual-test:\n    build: .\n    ports: [ \"5000:80\" ]\n"
  },
  {
    "path": "test/Ocelot.ManualTest/ocelot.identityserver4.json",
    "content": "﻿{\n  \"Routes\": [\n    {\n      \"DownstreamPathTemplate\": \"/\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"localhost\",\n          \"Port\": 52876\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/identityserverexample\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      },\n      \"AuthenticationOptions\": {\n        \"AuthenticationProviderKey\": \"TestKey\",\n        \"AllowedScopes\": [\n          \"openid\",\n          \"offline_access\"\n        ]\n      },\n      \"AddHeadersToRequest\": {\n        \"CustomerId\": \"Claims[CustomerId] > value\",\n        \"LocationId\": \"Claims[LocationId] > value\",\n        \"UserType\": \"Claims[sub] > value[0] > |\",\n        \"UserId\": \"Claims[sub] > value[1] > |\"\n      },\n      \"AddClaimsToRequest\": {\n        \"CustomerId\": \"Claims[CustomerId] > value\",\n        \"LocationId\": \"Claims[LocationId] > value\",\n        \"UserType\": \"Claims[sub] > value[0] > |\",\n        \"UserId\": \"Claims[sub] > value[1] > |\"\n      },\n      \"AddQueriesToRequest\": {\n        \"CustomerId\": \"Claims[CustomerId] > value\",\n        \"LocationId\": \"Claims[LocationId] > value\",\n        \"UserType\": \"Claims[sub] > value[0] > |\",\n        \"UserId\": \"Claims[sub] > value[1] > |\"\n      },\n      \"RouteClaimsRequirement\": {\n        \"UserType\": \"registered\"\n      },\n      \"RequestIdKey\": \"OcRequestId\"\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 443\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"HttpHandlerOptions\": {\n        \"AllowAutoRedirect\": true,\n        \"UseCookieContainer\": true\n      },\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts/{postId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/{postId}\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"RequestIdKey\": \"RouteRequestId\",\n      \"HttpHandlerOptions\": {\n        \"AllowAutoRedirect\": true,\n        \"UseCookieContainer\": true,\n        \"UseTracing\": true,\n        \"UseProxy\": true\n      },\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts/{postId}/comments\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/{postId}/comments\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"HttpHandlerOptions\": {\n        \"AllowAutoRedirect\": true,\n        \"UseCookieContainer\": true,\n        \"UseTracing\": false\n      },\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/comments\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/comments\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts\",\n      \"UpstreamHttpMethod\": [ \"Post\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts/{postId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/{postId}\",\n      \"UpstreamHttpMethod\": [ \"Put\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts/{postId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/{postId}\",\n      \"UpstreamHttpMethod\": [ \"Patch\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts/{postId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/{postId}\",\n      \"UpstreamHttpMethod\": [ \"Delete\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/api/products\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/products\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      },\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/api/products/{productId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/products/{productId}\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/api/products\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/products\",\n      \"UpstreamHttpMethod\": [ \"Post\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/api/products/{productId}\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/products/{productId}\",\n      \"UpstreamHttpMethod\": [ \"Put\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      },\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts\",\n      \"DownstreamScheme\": \"http\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 80\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/posts/\",\n      \"UpstreamHttpMethod\": [ \"Get\" ],\n      \"QoSOptions\": {\n        \"ExceptionsAllowedBeforeBreaking\": 3,\n        \"DurationOfBreak\": 10,\n        \"TimeoutValue\": 5000\n      },\n      \"FileCacheOptions\": { \"TtlSeconds\": 15 }\n    },\n    {\n      \"DownstreamPathTemplate\": \"/posts\",\n      \"DownstreamScheme\": \"https\",\n      \"DownstreamHostAndPorts\": [\n        {\n          \"Host\": \"jsonplaceholder.typicode.com\",\n          \"Port\": 443\n        }\n      ],\n      \"UpstreamPathTemplate\": \"/list-post\",\n      \"UpstreamHttpMethod\": [ \"GET\" ],\n      \"Metadata\": {\n        \"api_id\": \"e99d7ce0-d918-443e-b243-1960a8212b5d\"\n      }\n    }\n  ],\n\n  \"GlobalConfiguration\": {\n    \"RequestIdKey\": \"ot-traceid\"\n  }\n}\n"
  },
  {
    "path": "test/Ocelot.ManualTest/ocelot.json",
    "content": "﻿{\r\n  \"Routes\": [\r\n    {\r\n      \"DownstreamPathTemplate\": \"/posts/{postId}\",\r\n      \"DownstreamScheme\": \"http\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"jsonplaceholder.typicode.com\",\r\n          \"Port\": 80\r\n        }\r\n      ],\r\n      \"UpstreamPathTemplate\": \"/posts/{postId}\",\r\n      \"UpstreamHttpMethod\": [ \"Get\", \"Put\", \"Patch\", \"Delete\" ],\r\n      \"RequestIdKey\": \"RouteRequestId\",\r\n      \"HttpHandlerOptions\": {\r\n        \"AllowAutoRedirect\": true,\r\n        \"UseCookieContainer\": true,\r\n        \"UseTracing\": true,\r\n        \"UseProxy\": true\r\n      }\r\n    },\r\n    {\r\n      \"DownstreamPathTemplate\": \"/posts/{postId}/comments\",\r\n      \"DownstreamScheme\": \"http\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"jsonplaceholder.typicode.com\",\r\n          \"Port\": 80\r\n        }\r\n      ],\r\n      \"UpstreamPathTemplate\": \"/posts/{postId}/comments\",\r\n      \"UpstreamHttpMethod\": [ \"Get\" ],\r\n      \"HttpHandlerOptions\": {\r\n        \"AllowAutoRedirect\": true,\r\n        \"UseCookieContainer\": true,\r\n        \"UseTracing\": false\r\n      }\r\n    },\r\n    {\r\n      \"DownstreamPathTemplate\": \"/comments/{id}\",\r\n      \"DownstreamScheme\": \"http\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"jsonplaceholder.typicode.com\",\r\n          \"Port\": 80\r\n        }\r\n      ],\r\n      \"UpstreamPathTemplate\": \"/comments/{id}\",\r\n      \"UpstreamHttpMethod\": [ \"Get\" ],\r\n      \"Key\": \"C1\",\r\n      \"RateLimitOptions\": {\r\n        \"EnableRateLimiting\": true,\r\n        \"Limit\": 3,\r\n        \"Period\": \"1m\", // 1 minute\r\n        \"PeriodTimespan\": 3 // 3 seconds\r\n      }\r\n    },\r\n    {\r\n      \"DownstreamPathTemplate\": \"/posts\",\r\n      \"DownstreamScheme\": \"https\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"jsonplaceholder.typicode.com\",\r\n          \"Port\": 443\r\n        }\r\n      ],\r\n      \"UpstreamPathTemplate\": \"/metadata/posts\",\r\n      \"UpstreamHttpMethod\": [ \"GET\" ],\r\n      \"Metadata\": {\r\n        \"api_id\": \"e99d7ce0-d918-443e-b243-1960a8212b5d\"\r\n      }\r\n    },\r\n    {\r\n      \"UpstreamPathTemplate\": \"/bug930/ws1\",\r\n      \"DownstreamPathTemplate\": \"/WebSocket/EchoWebSocket.ashx\",\r\n      \"DownstreamScheme\": \"ws\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"corefx-net-http11.azurewebsites.net\",\r\n          \"Port\": 80\r\n        }\r\n      ],\r\n      \"Metadata\": {\r\n        \"bug_930\": \"https://github.com/ThreeMammals/Ocelot/issues/930\",\r\n        \"PR_2091\": \"https://github.com/ThreeMammals/Ocelot/pull/2091\"\r\n      }\r\n    },\r\n    {\r\n      \"UpstreamPathTemplate\": \"/bug930/ws2\",\r\n      \"DownstreamPathTemplate\": \"/\",\r\n      \"DownstreamScheme\": \"wss\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"echo.websocket.org\",\r\n          \"Port\": 443\r\n        }\r\n      ],\r\n      \"Metadata\": {\r\n        \"bug_930\": \"https://github.com/ThreeMammals/Ocelot/issues/930\",\r\n        \"PR_2091\": \"https://github.com/ThreeMammals/Ocelot/pull/2091\"\r\n      }\r\n    },\r\n    {\r\n      \"UpstreamPathTemplate\": \"/bug930/ws3\",\r\n      \"DownstreamPathTemplate\": \"/raw\",\r\n      \"DownstreamScheme\": \"wss\",\r\n      \"DownstreamHostAndPorts\": [\r\n        {\r\n          \"Host\": \"ws.postman-echo.com\",\r\n          \"Port\": 443\r\n        }\r\n      ],\r\n      \"Metadata\": {\r\n        \"bug_930\": \"https://github.com/ThreeMammals/Ocelot/issues/930\",\r\n        \"PR_2091\": \"https://github.com/ThreeMammals/Ocelot/pull/2091\"\r\n      }\r\n    }\r\n  ],\r\n\r\n  \"GlobalConfiguration\": {\r\n    \"RequestIdKey\": \"ot-traceid\",\r\n    \"RateLimitOptions\": {\r\n      \"ClientIdHeader\": \"MyRateLimiting\",\r\n      \"QuotaExceededMessage\": \"Customize Tips!\",\r\n      \"RateLimitCounterPrefix\": \"ocelot\",\r\n      \"Keys\": [\"C1\"]\r\n    },\r\n    \"RateLimitingRules\": [\r\n      {\r\n        \"Name\": \"MetadataRule\",\r\n        \"Pattern\": \"/metadata/posts\",\r\n        \"Methods\": [ \"GET\" ],\r\n        \"Limit\": 1,\r\n        \"Period\": \"1m\",\r\n        \"DisableRateLimitHeaders\": false,\r\n        \"QuotaExceededMessage\": \"You have exceeded the rate limit quota for grouped route by '/metadata/posts' pattern! Please try again later.\"\r\n      },\r\n      {\r\n        \"Name\": \"PostsRule\",\r\n        \"Pattern\": \"/posts/*\",\r\n        \"Methods\": [ \"GET\" ],\r\n        \"Limit\": 3,\r\n        \"Period\": \"1m\",\r\n        \"DisableRateLimitHeaders\": true,\r\n        \"QuotaExceededMessage\": \"You have exceeded the rate limit quota for grouped routes by '/posts/*' pattern! Please try again later.\"\r\n      }\r\n    ]\r\n  }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.ManualTest/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\"\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\"\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\"\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\"\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\"\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\"\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\"\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"8.0.1\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.0.1\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[10.0.5, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net10.0/win-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net8.0\": {\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"33eTIA2uO/L9utJjZWbKsMSVsQf7F8vtd6q5mQX7ZJzNvCpci5fleD6AeANGlbbb7WX7XKxq9+Dkb5e3GNDrmQ==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"cloLGeZolXbCJhJBc5OC05uhrdhdPL6MWHuVUnkkUvPDeK7HkwThBaLZ1XjBQVk9YhxXE2OvHXnKi0PLleXxDg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"YCxBt2EeJP8fcXk9desChkWI+0vFqFLvBwrz5hBMsoh0KJE6BC66DnzkdzkJNqMltLromc52dkdT206jJ38cTw==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"7.1.2\",\n          \"System.IdentityModel.Tokens.Jwt\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"oICJMqr3aNEDZOwnH5SK49bR6Z4aX0zEAnOLuhloumOSuqnNq+GWBdQyrgILnlcT5xj09xKCP/7Y7gJYB+ls/g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"Thhbe1peAmtSBFaV/ohtykXiZSOkx59Da44hvtWfIMFofDA3M3LaVyjstACf2rKGn4dEDR2cUpRAZ0Xs/zB+7Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[8.0.25, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net8.0/win-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0\": {\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\"\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\"\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"8.0.1\"\n        }\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.0.1\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[9.0.14, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0/win-x64\": {\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "test/Ocelot.ManualTest/tempkey.rsa",
    "content": "{\"KeyId\":\"6aad821b3e366a0189ea6c9741eae6ba\",\"Parameters\":{\"D\":\"RcfIF/iJ8qnKpHlaJCa9Qz+iN9Z655mfW0B/CycZx0WDQwQtjYNF+ijEkqfpGC3TJ9n19vXHdDEGfONxHwTtgS6PP/VIYYmql7OfCn+tkZUvMeIXykfEXFoNoWJXlT3eMI1JWyZpT/dZJLtmdeY09csDU/LjXTlyFrljW361T0NR/azAHqEfeuoKhqaJ2klzTzjif4xO5kMcTBHVyxrZm0+cbowsKPjI1QRh5xXsst8EDrM7rXStz4enneNaNbvP1nmWx++F7zn+5/WBDcPJVnL1HiyAzMAHj+oXG2JwDizO4RJxLbvQa2Y2jzoDp/qc++s2HWFo1PmuUnOzNIQjyQ==\",\"DP\":\"x3VwsILF1yo8puLB6TOcb4hMWngz8rqjBl3dty4E3Un7UexVh+NkqTiSXWZNern6Ka6gE8CpdDXhQiYCFbcnBiSF6FVSpjpQ+Qf9PeRj5HipJF+DzGyEOTzwOiBjb6s5CvPUQWvJiqQoP1D+1V+X/+C0zug8+Df73UyHA7uieKc=\",\"DQ\":\"aaZu9GfgKqUiy0uwPkmnLcwIEDqRlLG14c23OOoEqNRHK9T3OwUKEJ1qK0mbMKIVTwklyiC7uZHqMijIqk0LovTL4nI3LRlDkRjhUsIuubZZOAw7sYYemBUl+wEB/VZofaJ7H/CYtCUXyJhND2DFbTjzgeg3uWoMpyMDHuH/9Pc=\",\"Exponent\":\"AQAB\",\"InverseQ\":\"YRV4QA6rwB9BEHjzh5Wk3TcSS1CrwJWVopj/qC1amXxhtM3aMb4ZfKk7XoynLqHyQ86rB2p7dPNP2GL+fIWbt4h/ESO9JqOlM/bVXpdyCvIwchAL82hfHb4FRv+V+J2cksaIA+bHt7ye9n/XSmSr8v0WsjxN2qHzdla3t+J0c1I=\",\"Modulus\":\"xLJZzQyYbJAqymvvJeig9H4cfXEy3t0KVnRuUumdSBzU/3F7q1vCDBkXLqs8icEv9ZL4MUgDzzSjjYJpVXzvC24L1My3NLhSSZOCGrhSHCx98+cAgrb71tirXXsiBMXKeGhnJ05KopHtPRJVxBvd3d3Kee957y86g1Sbho1XxwWsrzVu5E7YZS+NkJycHkiUseMKeQ+tMLbPoFZPu5EqrrsSWuDjb7XNUjJViyGaOtvL2SQ9QtvDu006fe4m0VVw71ycSt2ReAmlA+EgCFsyLYBIoAhlk7k+lKyYO3a/8E0bzltB6MGaRaGJMB0C9pSxAfGSmSpIi2OUy0YIpIoDoQ==\",\"P\":\"ydx6DVoXf3DgS6WZtrR82xNf12kLD5cGUToPwwIjjX5oQywOhGXOV4GCrqISDff2bosrPvleBfuJ5KH9KRVAaEjh1At554Bq+Nw8cc/1mTXEOSKENDtA9GjkpthR0QW1FDFRR5Tc8sRuoBpulN1rJIDIkfEkqwlpugFmk2UrDk8=\",\"Q\":\"+XNIV8qMorQ11C1fVj4L91wufF4NqVqCdm/PN3f+xZ5UWoiCOil+njRuIL09ZifEwy3fgqD06Fu/SvaqMODyKAzA+RMUJU0sk92aOzAhKiGBk38sEvEuDUKZYNJm5NLjo9XXBG8DQzSUPvmIFLaMCloA95Ozie0mJcrXcimCww8=\"}}"
  },
  {
    "path": "test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Security.Claims;\nusing System.Security.Principal;\nusing System.Text;\nusing AuthenticationMiddleware = Ocelot.Authentication.AuthenticationMiddleware;\nusing AuthenticationOptions = Ocelot.Configuration.AuthenticationOptions;\n\nnamespace Ocelot.UnitTests.Authentication;\n\npublic class AuthenticationMiddlewareTests : UnitTest\n{\n    private readonly Mock<IAuthenticationService> _authentication;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IServiceProvider> _serviceProvider;\n    private readonly DefaultHttpContext _httpContext;\n\n    private AuthenticationMiddleware _middleware;\n    private RequestDelegate _next;\n    private bool _isNextCalled;\n\n    public AuthenticationMiddlewareTests()\n    {\n        _authentication = new Mock<IAuthenticationService>();\n        _serviceProvider = new Mock<IServiceProvider>();\n        _serviceProvider.Setup(sp => sp.GetService(typeof(IAuthenticationService))).Returns(_authentication.Object);\n        _httpContext = new DefaultHttpContext\n        {\n            RequestServices = _serviceProvider.Object,\n        };\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _factory.Setup(x => x.CreateLogger<AuthenticationMiddleware>()).Returns(_logger.Object);\n        _logger.Setup(x => x.LogInformation(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => _logInformationMessages.Add(f.Invoke()));\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => _logWarningMessages.Add(f.Invoke()));\n    }\n\n    [Fact]\n    public void MiddlewareName_Cstor_ReturnsTypeName()\n    {\n        // Arrange\n        _isNextCalled = false;\n        _next = (context) =>\n        {\n            _isNextCalled = true;\n            return Task.CompletedTask;\n        };\n        _middleware = new AuthenticationMiddleware(_next, _factory.Object);\n        var expected = _middleware.GetType().Name;\n\n        // Act\n        var actual = _middleware.MiddlewareName;\n\n        // Assert\n        Assert.False(_isNextCalled);\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public async Task Should_call_next_middleware_if_route_is_not_authenticated()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .WithAuthenticationOptions(new())\n            .Build();\n        GivenTheDownStreamRouteIs(route);\n\n        // Act\n        await WhenICallTheMiddleware(route.IsAuthenticated);\n\n        // Assert\n        ThenTheUserIsAuthenticated(\"The user is NOT authenticated\");\n    }\n\n    [Fact]\n    public async Task Should_call_next_middleware_if_route_is_using_options_method()\n    {\n        // Arrange\n        GivenTheDownStreamRouteIs(new DownstreamRouteBuilder()\n            .WithUpstreamHttpMethod([HttpMethods.Options])\n            .WithAuthenticationOptions(new())\n            .Build());\n        GivenTheRequestIsUsingMethod(HttpMethods.Options);\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsAuthenticated();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task Should_call_next_middleware_if_route_is_using_several_options_authentication_providers()\n    {\n        // Arrange\n        var multipleKeys = new string[] { string.Empty, \"Fail\", \"Test\" };\n        var options = new AuthenticationOptions(null, multipleKeys);\n        var methods = new List<string> { HttpMethods.Get };\n        GivenTheDownStreamRouteIs(new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(options)\n            .WithUpstreamHttpMethod(methods)\n            .Build());\n        GivenTheRequestIsUsingMethod(methods.First());\n        GivenTheAuthenticationIsFail();\n        GivenTheAuthenticationIsSuccess();\n        GivenTheAuthenticationThrowsException();\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsAuthenticated();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task Should_provide_backward_compatibility_if_route_has_several_options_authentication_providers()\n    {\n        // Arrange\n        FileAuthenticationOptions opts = new()\n        {\n            AuthenticationProviderKey = \"Test\",\n            AuthenticationProviderKeys = new[] { string.Empty, \"Fail\", \"Test\" },\n        };\n        AuthenticationOptions options = new(opts);\n        var methods = new List<string> { HttpMethods.Get };\n        GivenTheDownStreamRouteIs(new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(options)\n            .WithUpstreamHttpMethod(methods)\n            .Build());\n        GivenTheRequestIsUsingMethod(methods.First());\n        GivenTheAuthenticationIsFail();\n        GivenTheAuthenticationIsSuccess();\n        GivenTheAuthenticationThrowsException();\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsAuthenticated();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task Should_not_call_next_middleware_and_return_no_result_if_all_multiple_keys_were_failed()\n    {\n        // Arrange\n        var options = new AuthenticationOptions(null,\n            new[] { string.Empty, \"Fail\", \"Fail\", \"UnknownScheme\" });\n        var methods = new List<string> { HttpMethods.Get };\n        GivenTheDownStreamRouteIs(new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(options)\n            .WithUpstreamHttpMethod(methods)\n            .Build());\n        GivenTheRequestIsUsingMethod(methods.First());\n        GivenTheAuthenticationIsFail();\n        GivenTheAuthenticationIsSuccess();\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsNotAuthenticated();\n        _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse();\n        _logWarningMessages.Count.ShouldBe(1);\n        _logWarningMessages.First().ShouldStartWith(\"Client has NOT been authenticated for path\");\n        _httpContext.Items.Errors().First().ShouldBeOfType<UnauthenticatedError>();\n    }\n\n    private void GivenHappyPath(bool isHappy = true, string userName = null)\n    {\n        var multipleKeys = new string[] { \"Test\" };\n        var options = new AuthenticationOptions(null, multipleKeys);\n        string[] methods = [HttpMethods.Get];\n        GivenTheDownStreamRouteIs(new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(options)\n            .WithUpstreamHttpMethod(methods)\n            .Build());\n        GivenTheRequestIsUsingMethod(methods[0]);\n        GivenTheAuthenticationIsFail();\n        GivenTheAuthenticationIsSuccess(isHappy, userName); // Identity.IsAuthenticated -> true by default\n        GivenTheAuthenticationThrowsException();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task Should_SetUnauthenticatedError_and_not_call_next_middleware_if_identity_IsNOT_authenticated()\n    {\n        // Arrange\n        GivenHappyPath(false);// Identity.IsAuthenticated -> false\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsNotAuthenticated();\n        Assert.False(_isNextCalled);\n        _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse();\n        _logInformationMessages.Count.ShouldBe(1);\n        _logWarningMessages.Count.ShouldBe(1);\n        _logWarningMessages[0].ShouldBe(\"Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;\");\n        _httpContext.Items.Errors().Count.ShouldBe(1);\n        var e = _httpContext.Items.Errors()[0];\n        Assert.IsType<UnauthenticatedError>(e);\n        Assert.Equal(\"Request for authenticated route '' was unauthenticated!\", e.Message);\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"Igor\", 1)]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task SetUnauthenticatedError(string userName, int index)\n    {\n        // Arrange\n        var warnings = new string[]\n        {\n            \"Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;\",\n            \"Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' by 'Igor' was unauthenticated!;\",\n        };\n        var messages = new string[]\n        {\n            \"Request for authenticated route '' was unauthenticated!\",\n            \"Request for authenticated route '' by 'Igor' was unauthenticated!\",\n        };\n        GivenHappyPath(false, userName);// Identity.IsAuthenticated -> false\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsNotAuthenticated();\n        Assert.False(_isNextCalled);\n        Assert.Equal(warnings[index], _logWarningMessages[0]);\n        var e = _httpContext.Items.Errors()[0];\n        Assert.IsType<UnauthenticatedError>(e);\n        Assert.Equal(messages[index], e.Message);\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(2)]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task Should_not_call_next_middleware_and_return_no_result_if_providers_keys_are_empty(int keysCount)\n    {\n        // Arrange\n        var emptyKeys = new string[keysCount];\n        for (int i = 0; i < emptyKeys.Length; i++)\n        {\n            emptyKeys[i] = i % 2 == 0 ? null : string.Empty;\n        }\n\n        var optionsWithEmptyKeys = new AuthenticationOptions(null, emptyKeys);\n        var methods = new List<string> { \"Get\" };\n        var route = new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(optionsWithEmptyKeys)\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamPathTemplate(\"/\" + TestName())\n            .Build();\n        GivenTheDownStreamRouteIs(route);\n        GivenTheRequestIsUsingMethod(methods.First());\n\n        // Act\n        await WhenICallTheMiddleware(route.IsAuthenticated);\n\n        // Assert\n        ThenTheUserIsAuthenticated(\"The user is NOT authenticated\");\n        _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse();\n        _logWarningMessages.Count.ShouldBe(0);\n        _logInformationMessages.Count.ShouldBe(1);\n        _logInformationMessages[0].ShouldBe(\"No authentication is required for the path '' in the route /Should_not_call_next_middleware_and_return_no_result_if_providers_keys_are_empty.\");\n        _httpContext.Items.Errors().Count(e => e.GetType() == typeof(UnauthenticatedError)).ShouldBe(0);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"740\")] // https://github.com/ThreeMammals/Ocelot/issues/740\n    [Trait(\"Feat\", \"1580\")] // https://github.com/ThreeMammals/Ocelot/issues/1580\n    [Trait(\"PR\", \"1870\")] // https://github.com/ThreeMammals/Ocelot/pull/1870\n    public async Task AuthenticateAsync_CatchException()\n    {\n        // Arrange\n        GivenHappyPath(false);// Identity.IsAuthenticated -> false\n        _authentication\n            .Setup(a => a.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))\n            .Throws<HttpContext, string, InvalidOperationException>((ctx, scheme) => new(\"Bad auth scheme -> \" + scheme));\n\n        // Act\n        await WhenICallTheMiddleware();\n\n        // Assert\n        ThenTheUserIsNotAuthenticated();\n        Assert.False(_isNextCalled);\n        Assert.False(_httpContext.User.Identity.IsAuthenticated);\n        Assert.Equal(2, _logWarningMessages.Count);\n        Assert.Equal(\"Unable to authenticate the client for route '?' using the Test authentication scheme due to error: Bad auth scheme -> Test\", _logWarningMessages[0]);\n        Assert.Equal(\"Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;\", _logWarningMessages[1]);\n        var errors = _httpContext.Items.Errors();\n        Assert.Equal(1, errors.Count);\n        Assert.IsType<UnauthenticatedError>(errors[0]);\n        Assert.Equal(\"Request for authenticated route '' was unauthenticated!\", errors[0].Message);\n    }\n\n    private readonly List<string> _logInformationMessages = new();\n    private readonly List<string> _logWarningMessages = new();\n\n    private void GivenTheAuthenticationIsFail()\n    {\n        _authentication\n            .Setup(a => a.AuthenticateAsync(It.IsAny<HttpContext>(), It.Is<string>(s => s.Equals(\"Fail\"))))\n            .Returns(Task.FromResult(AuthenticateResult.Fail(\"The user is not authenticated.\")));\n    }\n\n    private void GivenTheAuthenticationIsSuccess(bool isAuthenticated = true, string userName = null)\n    {\n        var principal = new Mock<ClaimsPrincipal>();\n        var identity = new Mock<IIdentity>();\n        identity.Setup(i => i.IsAuthenticated).Returns(isAuthenticated);\n        identity.Setup(i => i.Name).Returns(userName ?? string.Empty);\n        principal.Setup(p => p.Identity).Returns(identity.Object);\n        _authentication\n            .Setup(a => a.AuthenticateAsync(It.IsAny<HttpContext>(), It.Is<string>(s => s.Equals(\"Test\"))))\n            .Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal.Object, \"Test\"))));\n    }\n\n    private void GivenTheAuthenticationThrowsException()\n    {\n        _authentication\n            .Setup(a => a.AuthenticateAsync(It.IsAny<HttpContext>(), It.Is<string>(scheme => string.Empty.Equals(scheme))))\n            .Throws(new InvalidOperationException(\"Authentication provider key is empty.\"));\n    }\n\n    private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)\n    {\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute);\n    }\n\n    private void GivenTheRequestIsUsingMethod(string method)\n    {\n        _httpContext.Request.Method = method;\n    }\n\n    private void ThenTheUserIsAuthenticated(string expected = null)\n    {\n        var content = _httpContext.Response.Body.AsString();\n        content.ShouldBe(expected ?? \"The user is authenticated\");\n    }\n\n    private void ThenTheUserIsNotAuthenticated(string expected = null)\n    {\n        var content = _httpContext.Response.Body.AsString();\n        var errors = _httpContext.Items.Errors();\n\n        content.ShouldBe(expected ?? string.Empty);\n        errors.ShouldNotBeEmpty();\n    }\n\n    private Task WhenICallTheMiddleware(bool isAuthenticated = true)\n    {\n        _isNextCalled = false;\n        _next = (context) =>\n        {\n            _isNextCalled = true;\n            var not = !isAuthenticated ? \" NOT\" : string.Empty;\n            byte[] byteArray = Encoding.ASCII.GetBytes($\"The user is{not} authenticated\");\n            var stream = new MemoryStream(byteArray);\n            _httpContext.Response.Body = stream;\n            return Task.CompletedTask;\n        };\n        _middleware = new AuthenticationMiddleware(_next, _factory.Object);\n        return _middleware.Invoke(_httpContext);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authentication/AuthenticationOptionsCreatorTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Authentication;\n\npublic class AuthenticationOptionsCreatorTests\n{\n    private readonly AuthenticationOptionsCreator _creator;\n\n    private readonly List<string> _routeScopes = new() { \"route scope 1\", \"route scope 2\" };\n    private const string _routeAuthProviderKey = \"route key\";\n    private readonly string[] _routeAuthProviderKeys = new string[] { \"route key 1\", \"route key 2\" };\n\n    private readonly List<string> _globalScopes = new() { \"global scope 1\", \"global scope 2\" };\n    private const string _globalAuthProviderKey = \"global key\";\n    private readonly string[] _globalAuthProviderKeys = new string[] { \"global key 1\", \"global key 2\" };\n\n    public AuthenticationOptionsCreatorTests()\n    {\n        _creator = new AuthenticationOptionsCreator();\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Create_OptionsObjIsNotNull_CreatedFromRoute(bool isAuthenticationProviderKeys)\n    {\n        string authenticationProviderKey = !isAuthenticationProviderKeys ? _routeAuthProviderKey : null;\n        string[] authenticationProviderKeys = isAuthenticationProviderKeys ? _routeAuthProviderKeys : null;\n        var route = CreateFileRoute(_routeScopes, authenticationProviderKey, authenticationProviderKeys);\n\n        // Act\n        var actual = _creator.Create(route, new());\n\n        // Assert\n        actual.AllowedScopes.ShouldBe(_routeScopes);\n        var expectedKeys = Enumerable\n            .Repeat(authenticationProviderKey, !isAuthenticationProviderKeys ? 1 : 0)\n            .Concat(authenticationProviderKeys ?? Enumerable.Empty<string>())\n            .ToArray();\n        actual.AuthenticationProviderKeys.ShouldBe(expectedKeys);\n    }\n\n    #region PR 2114\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    [Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\n    [Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\n    public void Create_GlobalAuthOptsObjIsNull_CreatedSuccessfully(bool isNull)\n    {\n        // Arrange\n        FileRoute arg1 = new();\n        FileGlobalConfiguration arg2 = new()\n        {\n            AuthenticationOptions = isNull ? new() : null,\n        };\n\n        // Act\n        var actual = _creator.Create(arg1, arg2);\n\n        // Assert\n        Assert.NotNull(actual);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2114\")]\n    [Trait(\"Feat\", \"842\")]\n    public void Create_GlobalConfigExists_ShouldUseGlobal()\n    {\n        // Arrange\n        var route = new FileRoute();\n        var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys);\n        var expected = new AuthenticationOptions(globalConfig.AuthenticationOptions);\n\n        // Act\n        var actual = _creator.Create(route, globalConfig);\n\n        // Assert\n        actual.AllowedScopes.ShouldBe(expected.AllowedScopes);\n        actual.AuthenticationProviderKeys.ShouldBe(expected.AuthenticationProviderKeys);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2114\")]\n    [Trait(\"Feat\", \"842\")]\n    public void Create_RouteKeyProviderEmpty_ShouldUseGlobal()\n    {\n        // Arrange\n        var route = CreateFileRoute(_routeScopes, string.Empty, null);\n        var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys);\n\n        // Act\n        var actual = _creator.Create(route, globalConfig);\n\n        // Assert\n        actual.AllowedScopes.ShouldBe(_routeScopes);\n        actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKey);\n        actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKeys[0]);\n        actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKeys[1]);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    [Trait(\"PR\", \"2114\")]\n    [Trait(\"Feat\", \"842\")]\n    public void Create_RouteAndGlobalKeyExist_ShouldUseRoute(bool routeHasSingleProviderKey)\n    {\n        // Arrange\n        var routeAuthProviderKey = routeHasSingleProviderKey ? _routeAuthProviderKey : null;\n        var routeAuthProviderKeys = routeHasSingleProviderKey ? null : _routeAuthProviderKeys;\n        var route = CreateFileRoute(_routeScopes, routeAuthProviderKey, routeAuthProviderKeys);\n        var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys);\n\n        // Act\n        var actual = _creator.Create(route, globalConfig);\n\n        // Assert\n        actual.AllowedScopes.ShouldBe(_routeScopes);\n        if (routeHasSingleProviderKey)\n        {\n            actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKey);\n            actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKeys[0]);\n            actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKeys[1]);\n        }\n        else\n        {\n            actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKey);\n            actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKeys[0]);\n            actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKeys[1]);\n        }\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2114\")]\n    [Trait(\"Feat\", \"842\")]\n    public void Create()\n    {\n        // Arrange\n        FileRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), ex.ParamName);\n        route = new();\n        ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), ex.ParamName);\n\n        globalConfiguration = new();\n        var actual = _creator.Create(route, globalConfiguration);\n        Assert.NotNull(actual);\n\n        route.AuthenticationOptions = globalConfiguration.AuthenticationOptions = null;\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.NotNull(actual);\n\n        route.AuthenticationOptions = new();\n        globalConfiguration.AuthenticationOptions = new()\n        {\n            AllowedScopes = [\"test\"],\n            AuthenticationProviderKeys = [\"test\"],\n        };\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.NotNull(actual);\n\n        route.AuthenticationOptions.AuthenticationProviderKeys = [\"test\"];\n        route.AuthenticationOptions.AllowedScopes = [\"test\"];\n        globalConfiguration.AuthenticationOptions.AuthenticationProviderKeys = [];\n        globalConfiguration.AuthenticationOptions.AllowedScopes = [];\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.NotNull(actual);\n    }\n    #endregion PR 2114\n\n    #region PR 2336\n    [Fact]\n    [Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\n    [Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n    public void Create_FileAuthenticationOptions()\n    {\n        // Arrange\n        FileAuthenticationOptions options = new()\n        {\n            AllowAnonymous = true,\n            AllowedScopes = new() { \"scope\" },\n            AuthenticationProviderKey = \"key1\",\n            AuthenticationProviderKeys = new[] { \"key2\" },\n        };\n\n        // Act\n        var actual = _creator.Create(options);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.True(actual.AllowAnonymous);\n        Assert.Contains(\"scope\", actual.AllowedScopes);\n        Assert.Contains(\"key1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"key2\", actual.AuthenticationProviderKeys);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void Create_FileRoute_ArgumentNullChecks()\n    {\n        // Arrange\n        FileRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n\n        // Act, Assert\n        var ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), ex.ParamName);\n\n        // Act, Assert\n        route = new();\n        ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), ex.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void Create_FromRoute()\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            AuthenticationOptions = new()\n            {\n                AllowAnonymous = false,\n                AllowedScopes = null,\n                AuthenticationProviderKey = \"route\",\n                AuthenticationProviderKeys = null,\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            AuthenticationOptions = new()\n            {\n                AllowAnonymous = null,\n                AllowedScopes = [\"global\"],\n                AuthenticationProviderKey = null,\n                AuthenticationProviderKeys = [\"global\"],\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.False(actual.AllowAnonymous);\n        Assert.Contains(\"global\", actual.AllowedScopes);\n        Assert.Contains(\"route\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"global\", actual.AuthenticationProviderKeys);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void Create_FromDynamicRoute_NullChecks()\n    {\n        // Arrange, Act, Assert\n        FileDynamicRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void Create_FromDynamicRoute()\n    {\n        // Arrange\n        FileDynamicRoute route = new()\n        {\n            AuthenticationOptions = new()\n            {\n                AllowAnonymous = false,\n                AllowedScopes = null,\n                AuthenticationProviderKey = \"route\",\n                AuthenticationProviderKeys = null,\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            AuthenticationOptions = new()\n            {\n                AllowAnonymous = null,\n                AllowedScopes = [\"global\"],\n                AuthenticationProviderKey = null,\n                AuthenticationProviderKeys = [\"global\"],\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.False(actual.AllowAnonymous);\n        Assert.Contains(\"global\", actual.AllowedScopes);\n        Assert.Contains(\"route\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"global\", actual.AuthenticationProviderKeys);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void CreateProtected_NullCheck()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        IRouteGrouping grouping = null;\n        FileAuthenticationOptions options = null;\n        FileGlobalAuthenticationOptions globalOptions = null;\n\n        // Act\n        var wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [grouping, options, globalOptions]));\n\n        // Assert\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        var actual = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(grouping), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2336\")]\n    [Trait(\"Feat\", \"2316\")]\n    public void CreateProtected()\n    {\n        // Arrange\n        FileAuthenticationOptions options = null;\n        FileDynamicRoute route = new()\n        {\n            Key = \"r1\",\n            AuthenticationOptions = options,\n        };\n        FileGlobalAuthenticationOptions globalOptions = new()\n        {\n            RouteKeys = null,\n            AllowAnonymous = null,\n            AllowedScopes = [\"global\"],\n            AuthenticationProviderKey = \"globalKey\",\n            AuthenticationProviderKeys = [\"global1\", \"global2\"],\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            AuthenticationOptions = globalOptions,\n        };\n\n        // Act, Assert\n        var actual = _creator.Create(route, globalConfiguration);\n        Assert.False(actual.AllowAnonymous);\n        Assert.Contains(\"global\", actual.AllowedScopes);\n        Assert.Contains(\"globalKey\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"global1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"global2\", actual.AuthenticationProviderKeys);\n\n        // Arrange 2\n        route.AuthenticationOptions = options = new()\n        {\n            AllowAnonymous = true,\n            AllowedScopes = [\"route\"],\n            AuthenticationProviderKey = \"route\",\n            AuthenticationProviderKeys = [\"route1\", \"route2\"],\n        };\n        globalOptions.RouteKeys = [\"?\"];\n\n        // Act, Assert 2\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.True(actual.AllowAnonymous);\n        Assert.Contains(\"route\", actual.AllowedScopes);\n        Assert.Contains(\"route\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route2\", actual.AuthenticationProviderKeys);\n\n        globalOptions.RouteKeys = [\"r1\"];\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.True(actual.AllowAnonymous);\n        Assert.Contains(\"route\", actual.AllowedScopes);\n        Assert.Contains(\"route\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route2\", actual.AuthenticationProviderKeys);\n\n        globalConfiguration.AuthenticationOptions = globalOptions = null;\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.True(actual.AllowAnonymous);\n        Assert.Contains(\"route\", actual.AllowedScopes);\n        Assert.Contains(\"route\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route2\", actual.AuthenticationProviderKeys);\n\n        // Arrange 3 : Merging\n        options.AllowAnonymous = null;\n        options.AllowedScopes = null;\n        options.AuthenticationProviderKey = null;\n        globalConfiguration.AuthenticationOptions = globalOptions = new()\n        {\n            RouteKeys = null,\n            AllowAnonymous = false,\n            AllowedScopes = [\"global\"],\n            AuthenticationProviderKey = \"globalKey\",\n            AuthenticationProviderKeys = [\"global1\", \"global2\"],\n        };\n        actual = _creator.Create(route, globalConfiguration);\n        Assert.False(actual.AllowAnonymous);\n        Assert.Contains(\"global\", actual.AllowedScopes);\n        Assert.Contains(\"globalKey\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route1\", actual.AuthenticationProviderKeys);\n        Assert.Contains(\"route2\", actual.AuthenticationProviderKeys);\n    }\n    #endregion PR 2336\n\n    private static FileRoute CreateFileRoute(List<string> allowedScopes, string authProviderKey, string[] authProviderKeys) => new()\n    {\n        AuthenticationOptions = new()\n        {\n            AllowedScopes = allowedScopes,\n            AuthenticationProviderKey = authProviderKey,\n            AuthenticationProviderKeys = authProviderKeys,\n        },\n    };\n\n    private static FileGlobalConfiguration CreateGlobalConfiguration(List<string> allowedScopes, string authProviderKey, string[] authProviderKeys) => new()\n    {\n        AuthenticationOptions = new()\n        {\n            AllowedScopes = allowedScopes,\n            AuthenticationProviderKey = authProviderKey,\n            AuthenticationProviderKeys = authProviderKeys,\n        },\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authentication/FileAuthenticationOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Authentication;\n\npublic class FileAuthenticationOptionsTests\n{\n    [Fact]\n    public void ToString_Serialized()\n    {\n        // Arrange\n        FileAuthenticationOptions opts = new()\n        {\n            AllowAnonymous = true,\n            AllowedScopes = [\"2\"],\n            AuthenticationProviderKey = \"3\",\n            AuthenticationProviderKeys = [\"4\", \"5\"],\n        };\n\n        // Act\n        var actual = opts.ToString();\n\n        // Assert\n        Assert.False(string.IsNullOrWhiteSpace(actual));\n        Assert.Equal(\"AllowAnonymous:True,AllowedScopes:['2'],AuthenticationProviderKey:'3',AuthenticationProviderKeys:['4','5']\", actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authentication/FileGlobalAuthenticationOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Authentication;\n\n[Trait(\"Feat\", \"585\")]\n[Trait(\"Feat\", \"2316\")] // https://github.com/ThreeMammals/Ocelot/issues/2316\n[Trait(\"PR\", \"2336\")] // https://github.com/ThreeMammals/Ocelot/pull/2336\npublic class FileGlobalAuthenticationOptionsTests\n{\n    [Fact]\n    public void Constructor_ShouldInitializeBaseAndRouteKeys()\n    {\n        // Act\n        var options = new FileGlobalAuthenticationOptions();\n\n        // Assert\n        Assert.NotNull(options);\n        Assert.Null(options.RouteKeys); // not initialized by default\n    }\n\n    [Fact]\n    public void Constructor_WithAuthScheme_ShouldSetAuthScheme()\n    {\n        // Arrange\n        var authScheme = \"TestScheme\";\n\n        // Act\n        var options = new FileGlobalAuthenticationOptions(authScheme);\n\n        // Assert\n        Assert.NotNull(options);\n        Assert.Single(options.AuthenticationProviderKeys, authScheme);\n    }\n\n    [Fact]\n    public void Constructor_WithFileAuthenticationOptions_ShouldCopyValues()\n    {\n        // Arrange\n        var from = new FileAuthenticationOptions()\n        {\n            AllowAnonymous = true,\n            AllowedScopes = [\"scope\"],\n            AuthenticationProviderKey = \"key\",\n            AuthenticationProviderKeys = [\"key1\", \"key2\"],\n        };\n\n        // Act\n        var options = new FileGlobalAuthenticationOptions(from);\n\n        // Assert\n        Assert.NotNull(options);\n        Assert.Null(options.RouteKeys);\n        FileAuthenticationOptions actual = (FileAuthenticationOptions)options;\n        Assert.Equivalent(from, actual);\n        Assert.Equal(from.AuthenticationProviderKey, options.AuthenticationProviderKey);\n        Assert.Contains(\"key1\", options.AuthenticationProviderKeys);\n        Assert.Contains(\"key2\", options.AuthenticationProviderKeys);\n    }\n\n    [Fact]\n    public void RouteKeys_ShouldAllowSetAndGet()\n    {\n        // Arrange\n        var options = new FileGlobalAuthenticationOptions();\n        var keys = new HashSet<string> { \"route1\", \"route2\" };\n\n        // Act\n        options.RouteKeys = keys;\n\n        // Assert\n        Assert.NotNull(options.RouteKeys);\n        Assert.Contains(\"route1\", options.RouteKeys);\n        Assert.Contains(\"route2\", options.RouteKeys);\n    }\n\n    [Fact]\n    public void ShouldImplementIRouteGroup()\n    {\n        // Act\n        var options = new FileGlobalAuthenticationOptions();\n\n        // Assert\n        Assert.True(options is IRouteGroup);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Authorization;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.Authorization;\n\npublic class AuthorizationMiddlewareTests : UnitTest\n{\n    private readonly Mock<IClaimsAuthorizer> _claimsAuthorizer;\n    private readonly Mock<IScopesAuthorizer> _scopesAuthorizer;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly AuthorizationMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public AuthorizationMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _claimsAuthorizer = new Mock<IClaimsAuthorizer>();\n        _scopesAuthorizer = new Mock<IScopesAuthorizer>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<AuthorizationMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _middleware = new AuthorizationMiddleware(_next, _claimsAuthorizer.Object, _scopesAuthorizer.Object, _loggerFactory.Object);\n\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(_warnings.Add);\n    }\n\n    private readonly List<Func<string>> _warnings = new();\n    private List<string> GetWarnings() => _warnings.Select(w => w()).ToList();\n\n    [Fact]\n    [Trait(\"Feat\", \"100\")] // https://github.com/ThreeMammals/Ocelot/issues/100\n    [Trait(\"PR\", \"104\")] // https://github.com/ThreeMammals/Ocelot/pull/104\n    [Trait(\"Release\", \"1.4.5\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5\n    public async Task Should_call_scopes_authorizer_when_route_is_authenticated()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build())\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .WithAuthenticationOptions(new(new(\"authScheme\")))\n            .Build();\n        GivenTheDownStreamRouteIs(new(), route);\n        GivenScopesAuthorizerReturns(new OkResponse<bool>(true));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenScopesAuthorizerIsCalled();\n    }\n\n    [Fact]\n    [Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\n    public async Task Should_call_authorization_service()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build())\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            /*.WithAuthenticationOptions(new(new(\"authScheme\")))*/\n            .WithRouteClaimsRequirement(new() { { \"k\", \"v\" } })\n            .Build();\n        GivenTheDownStreamRouteIs(new(), route);\n        GivenClaimsAuthorizerReturns(new OkResponse<bool>(true));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenClaimsAuthorizerIsCalled();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"100\")] // https://github.com/ThreeMammals/Ocelot/issues/100\n    [Trait(\"PR\", \"104\")] // https://github.com/ThreeMammals/Ocelot/pull/104\n    [Trait(\"Release\", \"1.4.5\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5\n    public async Task Invoke_RouteIsAuthenticated_WhenScopesAuthorizerError_ShouldUpsertErrors()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithAuthenticationOptions(new(new(\"authScheme\")))\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(\"/test\").Build())\n            .Build();\n        var response = new ErrorResponse<bool>(new ScopeNotAuthorizedError(\"No match\"));\n        GivenTheDownStreamRouteIs(new(), route);\n        GivenScopesAuthorizerReturns(response);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenScopesAuthorizerIsCalled();\n#if DEBUG\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once);\n        var warnings = GetWarnings();\n        Assert.Contains($\"The '/test' route encountered authorization errors due to user scopes:{Environment.NewLine}ScopeNotAuthorizedError: No match\", warnings);\n#endif\n        var errors = _httpContext.Items.Errors();\n        Assert.NotEmpty(errors);\n        Assert.Contains(response.Errors[0], errors);\n        var actual = Assert.Single(errors);\n        Assert.Same(response.Errors[0], actual);\n        Assert.Equal(\"No match\", actual.Message);\n    }\n\n    private void GivenTheDownStreamRouteIs(List<PlaceholderNameAndValue> templatePlaceholderNameAndValues, DownstreamRoute downstreamRoute)\n    {\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(templatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute);\n    }\n\n    private void GivenScopesAuthorizerReturns(Response<bool> expected) => _scopesAuthorizer\n            .Setup(x => x.Authorize(It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()))\n            .Returns(expected);\n\n    private void GivenClaimsAuthorizerReturns(Response<bool> expected) => _claimsAuthorizer\n            .Setup(x => x.Authorize(It.IsAny<ClaimsPrincipal>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<List<PlaceholderNameAndValue>>()))\n            .Returns(expected);\n\n    private void ThenScopesAuthorizerIsCalled(Func<Times> times = null)\n        => _scopesAuthorizer.Verify(\n            x => x.Authorize(It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()),\n            times ?? Times.Once);\n    private void ThenClaimsAuthorizerIsCalled(Func<Times> times = null)\n        => _claimsAuthorizer.Verify(\n            x => x.Authorize(It.IsAny<ClaimsPrincipal>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<List<PlaceholderNameAndValue>>()),\n            times ?? Times.Once);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs",
    "content": "﻿using Ocelot.Authorization;\r\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\r\nusing Ocelot.Infrastructure.Claims;\r\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\r\nnamespace Ocelot.UnitTests.Authorization;\r\n\r\npublic class ClaimsAuthorizerTests : UnitTest\r\n{\r\n    private readonly ClaimsAuthorizer _claimsAuthorizer;\r\n    private ClaimsPrincipal _claimsPrincipal;\r\n    private Dictionary<string, string> _requirement;\r\n    private List<PlaceholderNameAndValue> _urlPathPlaceholderNameAndValues;\r\n    private Response<bool> _result;\r\n\r\n    public ClaimsAuthorizerTests()\r\n    {\r\n        _claimsAuthorizer = new ClaimsAuthorizer(new ClaimsParser());\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_authorize_user()\r\n    {\r\n        // Arrange\r\n        GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\r\n        {\r\n            new(\"UserType\", \"registered\"),\r\n        })));\r\n        GivenARouteClaimsRequirement(new Dictionary<string, string>\r\n        {\r\n            {\"UserType\", \"registered\"},\r\n        });\r\n\r\n        // Act\r\n        WhenICallTheAuthorizer();\r\n\r\n        // Assert\r\n        ThenTheUserIsAuthorized();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_authorize_dynamic_user()\r\n    {\r\n        // Arrange\r\n        GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\r\n        {\r\n            new(\"userid\", \"14\"),\r\n        })));\r\n        GivenARouteClaimsRequirement(new Dictionary<string, string>\r\n        {\r\n            {\"userid\", \"{userId}\"},\r\n        });\r\n        GivenAPlaceHolderNameAndValueList(new List<PlaceholderNameAndValue>\r\n        {\r\n            new(\"{userId}\", \"14\"),\r\n        });\r\n\r\n        // Act\r\n        WhenICallTheAuthorizer();\r\n\r\n        // Assert\r\n        ThenTheUserIsAuthorized();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_authorize_dynamic_user()\r\n    {\r\n        // Arrange\r\n        GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\r\n        {\r\n            new(\"userid\", \"15\"),\r\n        })));\r\n        GivenARouteClaimsRequirement(new Dictionary<string, string>\r\n        {\r\n            {\"userid\", \"{userId}\"},\r\n        });\r\n        GivenAPlaceHolderNameAndValueList(new List<PlaceholderNameAndValue>\r\n        {\r\n            new(\"{userId}\", \"14\"),\r\n        });\r\n\r\n        // Act\r\n        WhenICallTheAuthorizer();\r\n\r\n        // Assert\r\n        ThenTheUserIsntAuthorized();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_authorize_user_multiple_claims_of_same_type()\r\n    {\r\n        // Arrange\r\n        GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\r\n        {\r\n            new(\"UserType\", \"guest\"),\r\n            new(\"UserType\", \"registered\"),\r\n        })));\r\n        GivenARouteClaimsRequirement(new Dictionary<string, string>\r\n        {\r\n            {\"UserType\", \"registered\"},\r\n        });\r\n\r\n        // Act\r\n        WhenICallTheAuthorizer();\r\n\r\n        // Assert\r\n        ThenTheUserIsAuthorized();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_authorize_user()\r\n    {\r\n        // Arrange\r\n        GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>())));\r\n        GivenARouteClaimsRequirement(new Dictionary<string, string>\r\n        {\r\n            { \"UserType\", \"registered\" },\r\n        });\r\n\r\n        // Act\r\n        WhenICallTheAuthorizer();\r\n\r\n        // Assert\r\n        ThenTheUserIsntAuthorized();\r\n    }\r\n\r\n    private void GivenAClaimsPrincipal(ClaimsPrincipal claimsPrincipal)\r\n    {\r\n        _claimsPrincipal = claimsPrincipal;\r\n    }\r\n\r\n    private void GivenARouteClaimsRequirement(Dictionary<string, string> requirement)\r\n    {\r\n        _requirement = requirement;\r\n    }\r\n\r\n    private void GivenAPlaceHolderNameAndValueList(List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues)\r\n    {\r\n        _urlPathPlaceholderNameAndValues = urlPathPlaceholderNameAndValues;\r\n    }\r\n\r\n    private void WhenICallTheAuthorizer()\r\n    {\r\n        _result = _claimsAuthorizer.Authorize(_claimsPrincipal, _requirement, _urlPathPlaceholderNameAndValues);\r\n    }\r\n\r\n    private void ThenTheUserIsAuthorized()\r\n    {\r\n        _result.Data.ShouldBe(true);\r\n    }\r\n\r\n    private void ThenTheUserIsntAuthorized()\r\n    {\r\n        _result.Data.ShouldBe(false);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authorization/UnauthorizedErrorTests.cs",
    "content": "﻿using Ocelot.Authorization;\nusing Ocelot.Errors;\n\nnamespace Ocelot.UnitTests.Authorization;\n\npublic class UnauthorizedErrorTests : UnitTest\n{\n    [Fact]\n    public void Ctor()\n    {\n        UnauthorizedError error = new(TestID);\n\n        Assert.Equal(TestID, error.Message);\n        Assert.Equal(OcelotErrorCode.UnauthorizedError, error.Code);\n        Assert.Equal(403, error.HttpStatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Authorization/UserDoesNotHaveClaimErrorTests.cs",
    "content": "﻿using Ocelot.Authorization;\nusing Ocelot.Errors;\n\nnamespace Ocelot.UnitTests.Authorization;\n\npublic class UserDoesNotHaveClaimErrorTests : UnitTest\n{\n    [Fact]\n    public void Ctor()\n    {\n        UserDoesNotHaveClaimError error = new(TestID);\n\n        Assert.Equal(TestID, error.Message);\n        Assert.Equal(OcelotErrorCode.UserDoesNotHaveClaimError, error.Code);\n        Assert.Equal(403, error.HttpStatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic class CacheOptionsCreatorTests : UnitTest\n{\n    private readonly CacheOptionsCreator _creator = new();\n\n    [Fact]\n    public void Should_create_region_from_loadBalancingKey()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            FileCacheOptions = new()\n            {\n                Region = string.Empty,\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, new FileGlobalConfiguration(), \"testKey\");\n\n        // Assert\n        Assert.Equal(\"testKey\", actual.Region);\n    }\n\n    [Fact]\n    public void Should_use_region()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            FileCacheOptions = new()\n            {\n                Region = \"region\",\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, new FileGlobalConfiguration(), \"bla-bla\");\n\n        // Assert\n        Assert.Equal(\"region\", actual.Region);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void ShouldCreateCacheOptions()\n    {\n        // Arrange\n        var options = GivenCacheOptions();\n        var route = GivenRoute(options);\n\n        // Act\n        var result = _creator.Create(route, new(), null);\n\n        // Assert\n        result.TtlSeconds.ShouldBe(options.TtlSeconds.Value);\n        result.Region.ShouldBe(options.Region);\n        result.Header.ShouldBe(options.Header);\n        result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void ShouldCreateCacheOptionsUsingGlobalConfiguration()\n    {\n        // Arrange\n        var global = GivenGlobalConfiguration();\n        var options = new FileCacheOptions();\n        var route = GivenRoute(options);\n\n        // Act\n        var result = _creator.Create(route, global, null);\n\n        // Assert\n        result.TtlSeconds.ShouldBe(global.CacheOptions.TtlSeconds.Value);\n        result.Region.ShouldBe(global.CacheOptions.Region);\n        result.Header.ShouldBe(global.CacheOptions.Header);\n        result.EnableContentHashing.ShouldBe(global.CacheOptions.EnableContentHashing.Value);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void RouteCacheOptionsShouldOverrideGlobalConfiguration()\n    {\n        // Arrange\n        var global = GivenGlobalConfiguration();\n        var options = GivenCacheOptions();\n        var route = GivenRoute(options);\n\n        // Act\n        var result = _creator.Create(route, global, null);\n\n        // Assert\n        result.TtlSeconds.ShouldBe(options.TtlSeconds.Value);\n        result.Region.ShouldBe(options.Region);\n        result.Header.ShouldBe(options.Header);\n        result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void ShouldCreateCacheOptionsWithDefaults()\n    {\n        // Arrange\n        var options = new FileCacheOptions();\n        var route = GivenRoute(options);\n\n        // Act\n        var result = _creator.Create(route, new(), \"testLbKey\");\n\n        // Assert\n        result.TtlSeconds.ShouldBe(0);\n        result.Region.ShouldBe(\"testLbKey\");\n        result.Header.ShouldBe(\"OC-Cache-Control\");\n        result.EnableContentHashing.ShouldBe(false);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2058\")]\n    [Trait(\"Bug\", \"2059\")]\n    public void ShouldComputeRegionIfNotProvided()\n    {\n        // Arrange\n        var global = GivenGlobalConfiguration();\n        var options = GivenCacheOptions();\n        var route = GivenRoute(options);\n        global.CacheOptions.Region = null;\n        options.Region = null;\n\n        // Act\n        var result = _creator.Create(route, global, \"testLbKey\");\n\n        // Assert\n        result.TtlSeconds.ShouldBe(options.TtlSeconds.Value);\n        result.Region.ShouldBe(\"testLbKey\");\n        result.Header.ShouldBe(options.Header);\n        result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void Create_FileCacheOptions()\n    {\n        // Arrange, Act, Assert : null\n        FileCacheOptions options = null;\n        var actual = _creator.Create(options);\n        Assert.NotNull(actual);\n        Assert.False(actual.UseCache);\n\n        // Arrange, Act, Assert : not null\n        options = GivenCacheOptions();\n        actual = _creator.Create(options);\n        Assert.Equal(options.TtlSeconds.Value, actual.TtlSeconds);\n        Assert.Equal(options.Region, actual.Region);\n        Assert.Equal(options.Header, actual.Header);\n        Assert.Equal(options.EnableContentHashing.Value, actual.EnableContentHashing);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void Create_FromRoute_NullChecks()\n    {\n        // Arrange, Act, Assert\n        FileRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration, \"lbKey\"));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration, \"lbKey\"));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void Create_FromRoute()\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            FileCacheOptions = new()\n            {\n                TtlSeconds = 1,\n                Region = \"route\",\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            CacheOptions = new()\n            {\n                TtlSeconds = 33,\n                Region = \"global\",\n            },\n        };\n\n        // Act, Assert : from FileCacheOptions\n        var actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n        Assert.Equal(1, actual.TtlSeconds);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"OC-Cache-Control\", actual.Header);\n        Assert.False(actual.EnableContentHashing);\n\n        // Arrange : from CacheOptions\n        route.FileCacheOptions = null;\n        route.CacheOptions = new()\n        {\n            TtlSeconds = 2,\n            Region = \"route\",\n            Header = \"fromCacheOptions\",\n            EnableContentHashing = true,\n        };\n\n        // Act, Assert : from CacheOptions\n        actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n        Assert.Equal(2, actual.TtlSeconds);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"fromCacheOptions\", actual.Header);\n        Assert.True(actual.EnableContentHashing);\n\n        // Arrange, Act, Assert : from route if not in the group\n        route.Key = \"bla-bla\";\n        globalConfiguration.CacheOptions.RouteKeys = [\"R1\"];\n        actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n        Assert.Equal(2, actual.TtlSeconds);\n        Assert.Equal(\"route\", actual.Region);\n\n        // Arrange, Act, Assert : from global\n        route.CacheOptions = null;\n        globalConfiguration.CacheOptions.RouteKeys.Clear();\n        actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n        Assert.Equal(33, actual.TtlSeconds);\n        Assert.Equal(\"global\", actual.Region);\n        Assert.Equal(\"OC-Cache-Control\", actual.Header);\n        Assert.False(actual.EnableContentHashing);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void Create_FromDynamicRoute_NullChecks()\n    {\n        // Arrange, Act, Assert\n        FileDynamicRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration, \"lbKey\"));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration, \"lbKey\"));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void Create_FromDynamicRoute()\n    {\n        // Arrange\n        FileDynamicRoute route = new()\n        {\n            CacheOptions = new()\n            {\n                TtlSeconds = 1,\n                Region = \"route\",\n                Header = \"route\",\n                EnableContentHashing = true,\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            CacheOptions = new()\n            {\n                TtlSeconds = 2,\n                Region = \"global\",\n                Header = \"global\",\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n\n        // Assert\n        Assert.Equal(1, actual.TtlSeconds);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"route\", actual.Header);\n        Assert.True(actual.EnableContentHashing);\n\n        // Arrange, Act, Assert : from global\n        route.CacheOptions = null;\n        actual = _creator.Create(route, globalConfiguration, \"lbKey\");\n        Assert.Equal(2, actual.TtlSeconds);\n        Assert.Equal(\"global\", actual.Region);\n        Assert.Equal(\"global\", actual.Header);\n        Assert.False(actual.EnableContentHashing);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void CreateProtected()\n    {\n        // Scenario 1: Null checks\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        IRouteGrouping grouping = null;\n        FileCacheOptions options = null;\n        FileGlobalCacheOptions globalOptions = null;\n        string loadBalancingKey = \"lbKey\";\n\n        // Act\n        var wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [grouping, options, globalOptions, loadBalancingKey]));\n\n        // Assert : Null checks\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        var actualEx = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(grouping), actualEx.ParamName);\n\n        // Scenario 2: if-else branches\n        FileDynamicRoute route = new() { Key = \"r1\" };\n        options = null;\n        globalOptions = new()\n        {\n            RouteKeys = null,\n            Region = \"global\",\n            Header = \"global\",\n            TtlSeconds = 3,\n        };\n\n        // Act, Assert\n        var actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n        Assert.Equal(\"global\", actual.Region);\n        Assert.Equal(\"global\", actual.Header);\n        Assert.Equal(3, actual.TtlSeconds);\n\n        // Arrange 2\n        options = new()\n        {\n            Region = \"route\",\n            Header = \"route\",\n            TtlSeconds = 1,\n        };\n        globalOptions.RouteKeys = [\"?\"];\n\n        // Act, Assert 2\n        actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"route\", actual.Header);\n        Assert.Equal(1, actual.TtlSeconds);\n\n        globalOptions.RouteKeys = [\"r1\"];\n        actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"route\", actual.Header);\n        Assert.Equal(1, actual.TtlSeconds);\n\n        globalOptions = null;\n        actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"route\", actual.Header);\n        Assert.Equal(1, actual.TtlSeconds);\n\n        // Arrange 3\n        options.Header = null;\n        globalOptions = new()\n        {\n            RouteKeys = null,\n            Region = \"global\",\n            Header = \"global\",\n            TtlSeconds = 5,\n        };\n        actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n        Assert.Equal(\"route\", actual.Region);\n        Assert.Equal(\"global\", actual.Header);\n        Assert.Equal(1, actual.TtlSeconds);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\n    public void CreateProtected_NoOptions()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        FileDynamicRoute route = new();\n        FileCacheOptions options = null;\n        FileGlobalCacheOptions globalOptions = null;\n        string loadBalancingKey = \"lbKey\";\n\n        // Act\n        var actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(0, actual.TtlSeconds);\n        Assert.Null(actual.Region);\n        Assert.Null(actual.Header);\n        Assert.False(actual.EnableContentHashing);\n        Assert.False(actual.UseCache);\n    }\n\n    private static FileGlobalConfiguration GivenGlobalConfiguration() => new()\n    {\n        CacheOptions = new()\n        {\n            TtlSeconds = 20,\n            Region = \"globalRegion\",\n            Header = \"globalHeader\",\n            EnableContentHashing = false,\n        },\n    };\n\n    private static FileRoute GivenRoute(FileCacheOptions options) => new()\n    {\n        FileCacheOptions = options,\n    };\n\n    private static FileCacheOptions GivenCacheOptions() => new()\n    {\n        TtlSeconds = 10,\n        Region = \"region\",\n        Header = \"header\",\n        EnableContentHashing = true,\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/CachedResponseTests.cs",
    "content": "﻿using Ocelot.Cache;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic class CachedResponseTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange\n        Dictionary<string, IEnumerable<string>> headers = new()\n        {\n            { \"header\", [\"headerValue\"] },\n        };\n\n        // Act\n        CachedResponse actual = new(HttpStatusCode.Created, headers, \"body\", headers, \"reasonPhrase\");\n\n        // Assert\n        Assert.Equal(HttpStatusCode.Created, actual.StatusCode);\n        Assert.NotEmpty(actual.Headers);\n        Assert.Contains(\"header\", actual.Headers);\n        Assert.NotEmpty(actual.ContentHeaders);\n        Assert.Contains(\"header\", actual.ContentHeaders);\n        Assert.Equal(\"body\", actual.Body);\n        Assert.Equal(\"reasonPhrase\", actual.ReasonPhrase);\n    }\n\n    [Fact]\n    public void Ctor_Defaulting()\n    {\n        // Arrange, Act\n        CachedResponse actual = new(HttpStatusCode.NotFound, null, null, null, \"reasonPhrase\");\n\n        // Assert\n        Assert.Equal(HttpStatusCode.NotFound, actual.StatusCode);\n        Assert.Empty(actual.Headers);\n        Assert.Empty(actual.ContentHeaders);\n        Assert.Empty(actual.Body);\n        Assert.Equal(\"reasonPhrase\", actual.ReasonPhrase);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Cache;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Request.Middleware;\nusing System.Reflection;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic sealed class DefaultCacheKeyGeneratorTests : UnitTest, IDisposable\n{\n    private readonly DefaultCacheKeyGenerator _generator;\n    private readonly HttpRequestMessage _request;\n\n    private readonly string verb = HttpMethods.Get;\n    private const string url = \"https://some.url/blah?abcd=123\";\n    private const string header = nameof(DefaultCacheKeyGeneratorTests);\n    private const string headerName = \"auth\";\n\n    public DefaultCacheKeyGeneratorTests()\n    {\n        _generator = new DefaultCacheKeyGenerator();\n        _request = new HttpRequestMessage\n        {\n            Method = new(verb),\n            RequestUri = new(url),\n        };\n        _request.Headers.Add(headerName, header);\n    }\n\n    [Fact]\n    public async Task Should_generate_cache_key_with_request_content()\n    {\n        // Arrange\n        const string noHeader = null;\n        const string content = nameof(Should_generate_cache_key_with_request_content);\n        var cachekey = MD5Helper.GenerateMd5($\"{verb}-{url}--{content}\");\n        var options = new CacheOptions(100, \"region\", noHeader, true);\n        var route = GivenDownstreamRoute(options);\n        _request.Content = new StringContent(content);\n\n        // Act\n        var generatedCacheKey = await WhenGenerateRequestCacheKey(route);\n\n        // Assert\n        generatedCacheKey.ShouldBe(cachekey);\n    }\n\n    [Fact]\n    public async Task Should_generate_cache_key_without_request_content()\n    {\n        // Arrange\n        CacheOptions options = null;\n        var cachekey = MD5Helper.GenerateMd5($\"{verb}-{url}\");\n        var route = GivenDownstreamRoute(options);\n\n        // Act\n        var generatedCacheKey = await WhenGenerateRequestCacheKey(route);\n\n        // Assert\n        generatedCacheKey.ShouldBe(cachekey);\n    }\n\n    [Fact]\n    public async Task Should_generate_cache_key_with_cache_options_header()\n    {\n        // Arrange\n        var options = new CacheOptions(100, \"region\", headerName, false);\n        var cachekey = MD5Helper.GenerateMd5($\"{verb}-{url}-{header}\");\n        var route = GivenDownstreamRoute(options);\n\n        // Act\n        var generatedCacheKey = await WhenGenerateRequestCacheKey(route);\n\n        // Assert\n        generatedCacheKey.ShouldBe(cachekey);\n\n        // Scenario 2: No header\n        _request.Headers.Clear();\n        cachekey = MD5Helper.GenerateMd5($\"{verb}-{url}-\");\n        generatedCacheKey = await WhenGenerateRequestCacheKey(route);\n        Assert.Equal(cachekey, generatedCacheKey);\n    }\n\n    [Fact]\n    public async Task Should_generate_cache_key_happy_path()\n    {\n        // Arrange\n        const string content = nameof(Should_generate_cache_key_happy_path);\n        var options = new CacheOptions(100, \"region\", headerName, true);\n        var cachekey = MD5Helper.GenerateMd5($\"{verb}-{url}-{header}-{content}\");\n        var route = GivenDownstreamRoute(options);\n        _request.Content = new StringContent(content);\n\n        // Act\n        var generatedCacheKey = await WhenGenerateRequestCacheKey(route);\n\n        // Assert\n        generatedCacheKey.ShouldBe(cachekey);\n    }\n\n    private static DownstreamRoute GivenDownstreamRoute(CacheOptions options) => new DownstreamRouteBuilder()\n        .WithKey(\"key1\")\n        .WithCacheOptions(options)\n        .Build();\n\n    private ValueTask<string> WhenGenerateRequestCacheKey(DownstreamRoute route)\n        => _generator.GenerateRequestCacheKey(new DownstreamRequest(_request), route);\n\n    public void Dispose()\n    {\n        _request.Dispose();\n    }\n}\n\ninternal class HttpContentStub : HttpContent\n{\n    private readonly MemoryStream _stream;\n\n    public HttpContentStub(string content)\n    {\n        _stream = new MemoryStream(Encoding.ASCII.GetBytes(content));\n\n        var field = typeof(HttpContent).GetField(\"_bufferedContent\", BindingFlags.NonPublic | BindingFlags.Instance);\n        field.SetValue(this, _stream);\n    }\n\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => throw new NotImplementedException();\n    protected override bool TryComputeLength(out long length) => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Cache;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic class DefaultMemoryCacheTests : UnitTest\n{\n    private readonly DefaultMemoryCache<Fake> _cache;\n    public DefaultMemoryCacheTests()\n    {\n        _ttl = TimeSpan.FromSeconds(100);\n        _cache = new DefaultMemoryCache<Fake>(new MemoryCache(new MemoryCacheOptions()));\n    }\n\n    protected TimeSpan TTL => _ttl;\n    private TimeSpan _ttl;\n\n    [Fact]\n    public void Should_cache()\n    {\n        // Arrange\n        var fake = new Fake(1);\n        _cache.Add(\"1\", fake, \"region\", TTL);\n\n        // Act\n        var result = _cache.Get(\"1\", \"region\");\n\n        // Assert\n        result.ShouldBe(fake);\n        fake.Value.ShouldBe(1);\n    }\n\n    [Fact]\n    public void Doesnt_exist()\n    {\n        // Arrange, Act\n        var result = _cache.Get(\"1\", \"region\");\n\n        // Assert\n        result.ShouldBeNull();\n    }\n\n    [Fact]\n    public void Should_add_or_update()\n    {\n        // Arrange\n        var fake = new Fake(1);\n        _cache.Add(\"1\", fake, \"region\", TTL);\n        var newFake = new Fake(1);\n        _cache.AddOrUpdate(\"1\", newFake, \"region\", TTL);\n\n        // Act\n        var result = _cache.Get(\"1\", \"region\");\n\n        // Assert\n        result.ShouldBe(newFake);\n        newFake.Value.ShouldBe(1);\n    }\n\n    [Fact]\n    public void Should_clear_region()\n    {\n        // Arrange\n        var fake1 = new Fake(1);\n        var fake2 = new Fake(2);\n        _cache.Add(\"1\", fake1, \"region\", TTL);\n        _cache.Add(\"2\", fake2, \"region\", TTL);\n        _cache.ClearRegion(\"region\");\n\n        // Act, Assert\n        var result1 = _cache.Get(\"1\", \"region\");\n        result1.ShouldBeNull();\n\n        // Act, Assert\n        var result2 = _cache.Get(\"2\", \"region\");\n        result2.ShouldBeNull();\n    }\n\n    [Fact]\n    public void Should_clear_key_if_ttl_expired()\n    {\n        // Arrange\n        var fake = new Fake(1);\n        _cache.Add(\"1\", fake, \"region\", TimeSpan.FromMilliseconds(50));\n        Thread.Sleep(200);\n\n        // Act\n        var result = _cache.Get(\"1\", \"region\");\n\n        // Assert\n        result.ShouldBeNull();\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-1)]\n    public void Should_not_add_to_cache_if_timespan_empty(int ttl)\n    {\n        // Arrange\n        var fake = new Fake(1);\n        _cache.Add(\"1\", fake, \"region\", TimeSpan.FromSeconds(ttl));\n\n        // Act\n        var result = _cache.Get(\"1\", \"region\");\n\n        // Assert\n        result.ShouldBeNull();\n    }\n\n    private class Fake\n    {\n        public Fake(int value)\n        {\n            Value = value;\n        }\n\n        public int Value { get; }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/FileCacheOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic class FileCacheOptionsTests\n{\n    [Fact]\n    public void Ctor_int()\n    {\n        var actual = new FileCacheOptions(3);\n        Assert.Equal(3, actual.TtlSeconds);\n        Assert.Null(actual.Region);\n        Assert.Null(actual.Header);\n        Assert.Null(actual.EnableContentHashing);\n    }\n\n    [Fact]\n    public void Ctor_FileCacheOptions()\n    {\n        FileCacheOptions from = new()\n        {\n            TtlSeconds = 4,\n            Region = \"region\",\n            Header = \"header\",\n            EnableContentHashing = true,\n        };\n        FileCacheOptions actual = new(from);\n\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n        Assert.Equal(4, actual.TtlSeconds);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/FileGlobalCacheOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Cache;\n\n[Trait(\"Feat\", \"585\")]\n[Trait(\"Feat\", \"2330\")] // https://github.com/ThreeMammals/Ocelot/issues/2330\npublic class FileGlobalCacheOptionsTests\n{\n    [Fact]\n    public void Ctor_int()\n    {\n        var actual = new FileGlobalCacheOptions(3);\n        Assert.Equal(3, actual.TtlSeconds);\n        Assert.Null(actual.Region);\n        Assert.Null(actual.Header);\n        Assert.Null(actual.EnableContentHashing);\n    }\n\n    [Fact]\n    public void Ctor_FileCacheOptions()\n    {\n        FileCacheOptions from = new()\n        {\n            TtlSeconds = 4,\n            Region = \"region\",\n            Header = \"header\",\n            EnableContentHashing = true,\n        };\n        FileGlobalCacheOptions actual = new(from);\n\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n        Assert.Equal(4, actual.TtlSeconds);\n        Assert.Null(actual.RouteKeys);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Cache;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Requester;\n\nnamespace Ocelot.UnitTests.Cache;\n\npublic class OutputCacheMiddlewareTests : UnitTest\n{\n    private readonly Mock<IOcelotCache<CachedResponse>> _cache = new();\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory = new();\n    private readonly Mock<IOcelotLogger> _logger = new();\n    private readonly Mock<ICacheKeyGenerator> _cacheGenerator = new();\n    private OutputCacheMiddleware _middleware;\n    private RequestDelegate _next;\n    private readonly ICacheKeyGenerator _cacheKeyGenerator;\n    private CachedResponse _response;\n    private readonly DefaultHttpContext _httpContext;\n    Func<string> _message;\n\n    public OutputCacheMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _cacheKeyGenerator = new DefaultCacheKeyGenerator();\n        _loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object);\n        _logger.Setup(x => x.LogDebug(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(m => _message = m);\n        _cacheGenerator.Setup(x => x.GenerateRequestCacheKey(It.IsAny<DownstreamRequest>(), It.IsAny<DownstreamRoute>()))\n            .Returns<DownstreamRequest, DownstreamRoute>((req, rou) => _cacheKeyGenerator.GenerateRequestCacheKey(req, rou));\n        _next = context => Task.CompletedTask;\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"https://some.url/blah?abcd=123\")));\n    }\n\n    [Fact]\n    public async Task Should_returned_cached_item_when_it_is_in_cache()\n    {\n        // Arrange\n        var headers = new Dictionary<string, IEnumerable<string>>\n        {\n            { \"test\", new List<string> { \"test\" } },\n        };\n        var contentHeaders = new Dictionary<string, IEnumerable<string>>\n        {\n            { \"content-type\", new List<string> { \"application/json\" } },\n        };\n        var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, string.Empty, contentHeaders, \"some reason\");\n        GivenThereIsACachedResponse(cachedResponse);\n        GivenTheDownstreamRouteIs();\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        ThenTheCacheGetIsCalledCorrectly();\n    }\n\n    [Fact]\n    public async Task Should_returned_cached_item_when_it_is_in_cache_expires_header()\n    {\n        // Arrange\n        var contentHeaders = new Dictionary<string, IEnumerable<string>>\n        {\n            { \"Expires\", new List<string> { \"-1\" } },\n        };\n        var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary<string, IEnumerable<string>>(), string.Empty, contentHeaders, \"some reason\");\n        GivenThereIsACachedResponse(cachedResponse);\n        GivenTheDownstreamRouteIs();\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        ThenTheCacheGetIsCalledCorrectly();\n    }\n\n    [Fact]\n    public async Task Should_continue_with_pipeline_and_cache_response()\n    {\n        // Arrange\n        GivenResponseIsNotCached(new HttpResponseMessage());\n        GivenTheDownstreamRouteIs();\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        ThenTheCacheAddIsCalled();\n    }\n\n    [Fact]\n    public async Task Should_not_use_cache()\n    {\n        // Arrange\n        GivenResponseIsNotCached(new HttpResponseMessage());\n        GivenTheDownstreamRouteIs(new CacheOptions(0, null, null, null));\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        _cacheGenerator.Verify(\n            x => x.GenerateRequestCacheKey(It.IsAny<DownstreamRequest>(), It.IsAny<DownstreamRoute>()),\n            Times.Never);\n    }\n\n    [Fact]\n    public async Task Should_not_add_to_cache_when_errors()\n    {\n        // Arrange\n        GivenResponseIsNotCached(new HttpResponseMessage());\n        GivenTheDownstreamRouteIs();\n        _next = static context =>\n        {\n            context.Items.UpsertErrors([new RequestCanceledError(\"Bla-bla message\")]);\n            return Task.CompletedTask;\n        };\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        ThenTheCacheAddIsCalled(Times.Never);\n        ThenTheMessageIs(\"There was a pipeline error for the 'GET-https://some.url/blah?abcd=123' key.\");\n    }\n\n    [Fact]\n    public async Task CreateHttpResponseMessage_CachedIsNull()\n    {\n        // Arrange\n        CachedResponse cached = null;\n        GivenThereIsACachedResponse(cached);\n        GivenTheDownstreamRouteIs();\n\n        // Act\n        await WhenICallTheMiddlewareAsync();\n\n        // Assert\n        ThenTheCacheGetIsCalledCorrectly();\n    }\n\n    private void ThenTheMessageIs(string expected)\n    {\n        Assert.NotNull(_message);\n        var msg = _message.Invoke();\n        Assert.Equal(expected, msg);\n    }\n\n    private async Task WhenICallTheMiddlewareAsync()\n    {\n        _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheGenerator.Object);\n        await _middleware.Invoke(_httpContext);\n    }\n\n    private void GivenThereIsACachedResponse(CachedResponse response)\n    {\n        _response = response;\n        _cache\n          .Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>()))\n          .Returns(_response);\n    }\n\n    private void GivenResponseIsNotCached(HttpResponseMessage responseMessage)\n    {\n        _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(responseMessage));\n    }\n\n    private void GivenTheDownstreamRouteIs(CacheOptions options = null)\n    {\n        var downRoute = new DownstreamRouteBuilder()\n            .WithCacheOptions(options ?? new(100, \"kanken\", null, false))\n            .WithUpstreamHttpMethod([ \"Get\" ])\n            .Build();\n        var route = new Route(downRoute)\n        {\n            UpstreamHttpMethod = [HttpMethod.Get],\n        };\n        var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List<PlaceholderNameAndValue>(), route);\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n    }\n\n    private void ThenTheCacheGetIsCalledCorrectly()\n    {\n        _cache.Verify(\n            x => x.Get(It.IsAny<string>(), It.IsAny<string>()),\n            Times.Once);\n    }\n\n    private void ThenTheCacheAddIsCalled(Func<Times> howMany = null)\n    {\n        _cache.Verify(\n            x => x.Add(It.IsAny<string>(), It.IsAny<CachedResponse>(), It.IsAny<string>(), It.IsAny<TimeSpan>()),\n            howMany ?? Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Claims;\nusing Ocelot.Configuration;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.Claims;\n\npublic class AddClaimsToRequestTests : UnitTest\n{\n    private readonly AddClaimsToRequest _addClaimsToRequest;\n    private readonly Mock<IClaimsParser> _parser;\n    private List<ClaimToThing> _claimsToThings;\n    private HttpContext _context;\n    private Response _result;\n    private Response<string> _claimValue;\n\n    public AddClaimsToRequestTests()\n    {\n        _parser = new Mock<IClaimsParser>();\n        _addClaimsToRequest = new AddClaimsToRequest(_parser.Object);\n    }\n\n    [Fact]\n    public void Should_add_claims_to_context()\n    {\n        // Arrange\n        var context = new DefaultHttpContext\n        {\n            User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\n            {\n                new(\"test\", \"data\"),\n            })),\n        };\n        GivenClaimsToThings(new List<ClaimToThing>\n        {\n            new(\"claim-key\", string.Empty, string.Empty, 0),\n        });\n        GivenHttpContext(context);\n        GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        WhenIAddClaimsToTheRequest();\n\n        // Assert\n        ThenTheResultIsSuccess();\n    }\n\n    [Fact]\n    public void If_claims_exists_should_replace_it()\n    {\n        // Arrange\n        var context = new DefaultHttpContext\n        {\n            User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>\n            {\n                new(\"existing-key\", \"data\"),\n                new(\"new-key\", \"data\"),\n            })),\n        };\n        GivenClaimsToThings(new List<ClaimToThing>\n        {\n            new(\"existing-key\", \"new-key\", string.Empty, 0),\n        });\n        GivenHttpContext(context);\n        GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        WhenIAddClaimsToTheRequest();\n\n        // Assert\n        ThenTheResultIsSuccess();\n    }\n\n    [Fact]\n    public void Should_return_error()\n    {\n        // Arrange\n        GivenClaimsToThings(new List<ClaimToThing>\n        {\n            new(string.Empty, string.Empty, string.Empty, 0),\n        });\n        GivenHttpContext(new DefaultHttpContext());\n        GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error>\n        {\n            new AnyError(),\n        }));\n\n        // Act\n        WhenIAddClaimsToTheRequest();\n\n        // Assert\n        ThenTheResultIsError();\n    }\n\n    private void GivenClaimsToThings(List<ClaimToThing> configuration)\n    {\n        _claimsToThings = configuration;\n    }\n\n    private void GivenHttpContext(HttpContext context)\n    {\n        _context = context;\n    }\n\n    private void GivenTheClaimParserReturns(Response<string> claimValue)\n    {\n        _claimValue = claimValue;\n        _parser\n            .Setup(\n                x =>\n                    x.GetValue(It.IsAny<IEnumerable<Claim>>(),\n                    It.IsAny<string>(),\n                    It.IsAny<string>(),\n                    It.IsAny<int>()))\n            .Returns(_claimValue);\n    }\n\n    private void WhenIAddClaimsToTheRequest()\n    {\n        _result = _addClaimsToRequest.SetClaimsOnContext(_claimsToThings, _context);\n    }\n\n    private void ThenTheResultIsSuccess()\n    {\n        _result.IsError.ShouldBe(false);\n    }\n\n    private void ThenTheResultIsError()\n    {\n        _result.IsError.ShouldBe(true);\n    }\n\n    private class AnyError : Error\n    {\n        public AnyError()\n            : base(\"blahh\", OcelotErrorCode.UnknownError, 404)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Claims;\nusing Ocelot.Claims.Middleware;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Claims;\n\npublic class ClaimsToClaimsMiddlewareTests : UnitTest\n{\n    private readonly Mock<IAddClaimsToRequest> _addHeaders;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ClaimsToClaimsMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ClaimsToClaimsMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _addHeaders = new Mock<IAddClaimsToRequest>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ClaimsToClaimsMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _middleware = new ClaimsToClaimsMiddleware(_next, _loggerFactory.Object, _addHeaders.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_claims_to_request_correctly()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"any old string\")\n            .WithClaimsToClaims(new()\n            {\n                new(\"sub\", \"UserType\", \"|\", 0),\n            })\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route()\n            {\n                DownstreamRoute = [route],\n                UpstreamHttpMethod = [HttpMethod.Get],\n            });\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenTheAddClaimsToRequestReturns();\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheClaimsToRequestIsCalledCorrectly();\n    }\n\n    private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute)\n    {\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n    }\n\n    private void GivenTheAddClaimsToRequestReturns()\n    {\n        _addHeaders\n            .Setup(x => x.SetClaimsOnContext(It.IsAny<List<ClaimToThing>>(),\n            It.IsAny<HttpContext>()))\n            .Returns(new OkResponse());\n    }\n\n    private void ThenTheClaimsToRequestIsCalledCorrectly()\n    {\n        _addHeaders\n            .Verify(x => x.SetClaimsOnContext(It.IsAny<List<ClaimToThing>>(),\n            It.IsAny<HttpContext>()), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Values;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class AggregatesCreatorTests : UnitTest\r\n{\r\n    private readonly AggregatesCreator _creator;\r\n    private readonly Mock<IUpstreamTemplatePatternCreator> _utpCreator;\n    private readonly Mock<IUpstreamHeaderTemplatePatternCreator> _uhtpCreator;\r\n    private FileConfiguration _fileConfiguration;\r\n    private List<Route> _routes;\r\n    private List<Route> _result;\r\n    private UpstreamPathTemplate _aggregate1Utp;\r\n    private UpstreamPathTemplate _aggregate2Utp;\n    private Dictionary<string, UpstreamHeaderTemplate> _headerTemplates1;\n    private Dictionary<string, UpstreamHeaderTemplate> _headerTemplates2;\r\n\r\n    public AggregatesCreatorTests()\r\n    {\r\n        _utpCreator = new Mock<IUpstreamTemplatePatternCreator>();\n        _uhtpCreator = new Mock<IUpstreamHeaderTemplatePatternCreator>();\r\n        _creator = new AggregatesCreator(_utpCreator.Object, _uhtpCreator.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_no_aggregates()\r\n    {\r\n        // Arrange\r\n        var fileConfig = new FileConfiguration\r\n        {\r\n            Aggregates = new List<FileAggregateRoute>\r\n            {\r\n                new()\r\n                {\r\n                    RouteKeys = new([\"key1\"]),\r\n                },\r\n            },\r\n        };\r\n        var routes = new List<Route>();\r\n        GivenThe(fileConfig);\r\n        GivenThe(routes);\r\n\r\n        // Act\r\n        WhenICreate();\r\n\r\n        // Assert\r\n        TheUtpCreatorIsNotCalled();\r\n        ThenTheResultIsNotNull();\r\n        ThenTheResultIsEmpty();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_aggregates()\r\n    {\r\n        // Arrange\r\n        var fileConfig = new FileConfiguration\r\n        {\r\n            Aggregates = new List<FileAggregateRoute>\r\n            {\r\n                new()\r\n                {\r\n                    RouteKeys = new([\"key1\", \"key2\"]),\r\n                    UpstreamHost = \"hosty\",\r\n                    UpstreamPathTemplate = \"templatey\",\r\n                    Aggregator = \"aggregatory\",\r\n                    RouteIsCaseSensitive = true,\r\n                },\r\n                new()\r\n                {\r\n                    RouteKeys = new([\"key3\", \"key4\"]),\r\n                    UpstreamHost = \"hosty\",\r\n                    UpstreamPathTemplate = \"templatey\",\r\n                    Aggregator = \"aggregatory\",\r\n                    RouteIsCaseSensitive = true,\r\n                },\r\n            },\r\n        };\r\n        var routes = new List<Route>\r\n        {\r\n            new(new DownstreamRouteBuilder().WithKey(\"key1\").Build()),\r\n            new(new DownstreamRouteBuilder().WithKey(\"key2\").Build()),\r\n            new(new DownstreamRouteBuilder().WithKey(\"key3\").Build()),\r\n            new(new DownstreamRouteBuilder().WithKey(\"key4\").Build()),\r\n        };\r\n\r\n        GivenThe(fileConfig);\r\n        GivenThe(routes);\r\n        GivenTheUtpCreatorReturns();\r\n        GivenTheUhtpCreatorReturns();\r\n\r\n        // Act\r\n        WhenICreate();\r\n\r\n        // Assert\r\n        ThenTheUtpCreatorIsCalledCorrectly();\r\n        ThenTheAggregatesAreCreated();\r\n    }\r\n\r\n    private void ThenTheAggregatesAreCreated()\r\n    {\r\n        _result.ShouldNotBeNull();\r\n        _result.Count.ShouldBe(2);\r\n\r\n        _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get);\r\n        _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost);\r\n        _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp);\n        _result[0].UpstreamHeaderTemplates.ShouldBe(_headerTemplates1);\r\n        _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator);\r\n        _result[0].DownstreamRoute.ShouldContain(x => x == _routes[0].DownstreamRoute[0]);\r\n        _result[0].DownstreamRoute.ShouldContain(x => x == _routes[1].DownstreamRoute[0]);\r\n\r\n        _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get);\r\n        _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost);\r\n        _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp);\n        _result[1].UpstreamHeaderTemplates.ShouldBe(_headerTemplates2);\r\n        _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator);\r\n        _result[1].DownstreamRoute.ShouldContain(x => x == _routes[2].DownstreamRoute[0]);\r\n        _result[1].DownstreamRoute.ShouldContain(x => x == _routes[3].DownstreamRoute[0]);\r\n    }\r\n\r\n    private void ThenTheUtpCreatorIsCalledCorrectly()\r\n    {\r\n        _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[0]), Times.Once);\r\n        _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[1]), Times.Once);\r\n    }\r\n\r\n    private void GivenTheUtpCreatorReturns()\r\n    {\r\n        _aggregate1Utp = new UpstreamPathTemplateBuilder().Build();\r\n        _aggregate2Utp = new UpstreamPathTemplateBuilder().Build();\r\n\r\n        _utpCreator.SetupSequence(x => x.Create(It.IsAny<IRouteUpstream>()))\r\n            .Returns(_aggregate1Utp)\r\n            .Returns(_aggregate2Utp);\r\n    }\n\n    private void GivenTheUhtpCreatorReturns()\r\n    {\r\n        _headerTemplates1 = new Dictionary<string, UpstreamHeaderTemplate>();\r\n        _headerTemplates2 = new Dictionary<string, UpstreamHeaderTemplate>();\r\n\r\n        _uhtpCreator.SetupSequence(x => x.Create(It.IsAny<IRouteUpstream>()))\r\n            .Returns(_headerTemplates1)\r\n            .Returns(_headerTemplates2);\r\n    }\r\n\r\n    private void ThenTheResultIsEmpty()\r\n    {\r\n        _result.Count.ShouldBe(0);\r\n    }\r\n\r\n    private void ThenTheResultIsNotNull()\r\n    {\r\n        _result.ShouldNotBeNull();\r\n    }\r\n\r\n    private void TheUtpCreatorIsNotCalled()\r\n    {\r\n        _utpCreator.Verify(x => x.Create(It.IsAny<FileAggregateRoute>()), Times.Never);\r\n    }\r\n\r\n    private void GivenThe(FileConfiguration fileConfiguration)\r\n    {\r\n        _fileConfiguration = fileConfiguration;\r\n    }\r\n\r\n    private void GivenThe(List<Route> routes)\r\n    {\r\n        _routes = routes;\r\n    }\r\n\r\n    private void WhenICreate()\r\n    {\r\n        _result = _creator.Create(_fileConfiguration, _routes);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs",
    "content": "using Ocelot.Configuration.ChangeTracking;\n\r\nnamespace Ocelot.UnitTests.Configuration.ChangeTracking;\r\n\r\npublic class OcelotConfigurationChangeTokenSourceTests : UnitTest\r\n{\r\n    private readonly OcelotConfigurationChangeTokenSource _source;\r\n\r\n    public OcelotConfigurationChangeTokenSourceTests()\r\n    {\r\n        _source = new OcelotConfigurationChangeTokenSource();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_activate_change_token()\r\n    {\r\n        // Arrange, Act\r\n        _source.Activate();\r\n\r\n        // Assert\r\n        _source.ChangeToken.HasChanged.ShouldBeTrue();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs",
    "content": "using Ocelot.Configuration.ChangeTracking;\n\r\nnamespace Ocelot.UnitTests.Configuration.ChangeTracking;\r\n\r\npublic class OcelotConfigurationChangeTokenTests : UnitTest\r\n{\r\n    [Fact]\r\n    public void Should_call_callback_with_state()\r\n    {\r\n        // Arrange\r\n        GivenIHaveAChangeToken();\r\n        AndIRegisterACallback();\r\n        ThenIShouldGetADisposableWrapper();\r\n\r\n        // Act\r\n        GivenIActivateTheToken();\r\n\r\n        // Assert\r\n        ThenTheCallbackShouldBeCalled();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_call_callback_if_it_is_disposed()\r\n    {\r\n        // Arrange\r\n        GivenIHaveAChangeToken();\r\n        AndIRegisterACallback();\r\n        ThenIShouldGetADisposableWrapper();\r\n\r\n        // Act, Assert\r\n        GivenIActivateTheToken();\r\n        AndIDisposeTheCallbackWrapper();\r\n\r\n        // Act, Assert\r\n        GivenIActivateTheToken();\r\n        ThenTheCallbackShouldNotBeCalled();\r\n    }\r\n\r\n    private OcelotConfigurationChangeToken _changeToken;\r\n    private IDisposable _callbackWrapper;\r\n    private int _callbackCounter;\r\n    private readonly object _callbackInitialState = new();\r\n    private object _callbackState;\r\n\r\n    private void Callback(object state)\r\n    {\r\n        _callbackCounter++;\r\n        _callbackState = state;\r\n        _changeToken.HasChanged.ShouldBeTrue();\r\n    }\r\n\r\n    private void GivenIHaveAChangeToken()\r\n    {\r\n        _changeToken = new OcelotConfigurationChangeToken();\r\n    }\r\n\r\n    private void AndIRegisterACallback()\r\n    {\r\n        _callbackWrapper = _changeToken.RegisterChangeCallback(Callback, _callbackInitialState);\r\n    }\r\n\r\n    private void ThenIShouldGetADisposableWrapper()\r\n    {\r\n        _callbackWrapper.ShouldNotBeNull();\r\n    }\r\n\r\n    private void GivenIActivateTheToken()\r\n    {\r\n        _callbackCounter = 0;\r\n        _callbackState = null;\r\n        _changeToken.Activate();\r\n    }\r\n\r\n    private void ThenTheCallbackShouldBeCalled()\r\n    {\r\n        _callbackCounter.ShouldBe(1);\r\n        _callbackState.ShouldNotBeNull();\r\n        _callbackState.ShouldBeSameAs(_callbackInitialState);\r\n    }\r\n\r\n    private void ThenTheCallbackShouldNotBeCalled()\r\n    {\r\n        _callbackCounter.ShouldBe(0);\r\n        _callbackState.ShouldBeNull();\r\n    }\r\n\r\n    private void AndIDisposeTheCallbackWrapper()\r\n    {\r\n        _callbackState = null;\r\n        _callbackCounter = 0;\r\n        _callbackWrapper.Dispose();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationMonitorTests.cs",
    "content": "﻿using Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Configuration.ChangeTracking;\n\npublic class OcelotConfigurationMonitorTests : UnitTest\n{\n    private Mock<IInternalConfigurationRepository> _mConfigurationRepo;\n    private Mock<IOcelotConfigurationChangeTokenSource> _mChangeTokenSource;\n    private Mock<IChangeToken> _mChangeToken;\n    private IInternalConfiguration _testConfiguration;\n    private Response<IInternalConfiguration> _repoResponse;\n    private OcelotConfigurationMonitor _monitor;\n\n    public OcelotConfigurationMonitorTests()\n    {\n        GivenTheRepositoryMock();\n        GivenTheChangeTokenSourceMock();\n        GivenTheChangeTokenMock();\n    }\n\n    [Fact]\n    public void Constructor_WithValidDependencies_ShouldCreateInstance()\n    {\n        // Arrange\n        var repo = _mConfigurationRepo.Object;\n        var changeTokenSource = _mChangeTokenSource.Object;\n\n        // Act\n        var monitor = new OcelotConfigurationMonitor(repo, changeTokenSource);\n\n        // Assert\n        Assert.NotNull(monitor);\n        Assert.IsType<OcelotConfigurationMonitor>(monitor);\n    }\n\n    [Fact]\n    public void Constructor_WithNullRepository_ShouldThrowArgumentNullException()\n    {\n        // Arrange\n        IInternalConfigurationRepository repo = null;\n\n        // Act & Assert\n        var e = Assert.Throws<ArgumentNullException>(\n            () => new OcelotConfigurationMonitor(repo, _mChangeTokenSource.Object));\n\n        // Assert\n        Assert.NotNull(e);\n        Assert.Equal(nameof(repo), e.ParamName);\n    }\n\n    [Fact]\n    public void Constructor_WithNullChangeTokenSource_ShouldThrowArgumentNullException()\n    {\n        // Arrange\n        IOcelotConfigurationChangeTokenSource changeTokenSource = null;\n\n        // Act & Assert\n        var e = Assert.Throws<ArgumentNullException>(\n            () => new OcelotConfigurationMonitor(_mConfigurationRepo.Object, changeTokenSource));\n\n        // Assert\n        Assert.NotNull(e);\n        Assert.Equal(nameof(changeTokenSource), e.ParamName);\n    }\n\n    [Fact]\n    public void CurrentValue_WhenCalled_ShouldReturnConfigurationFromRepository()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n\n        // Act\n        var result = _monitor.CurrentValue;\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(_testConfiguration, result);\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void Get_WithValidName_ShouldReturnConfigurationFromRepository()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        const string configName = \"test-config\";\n\n        // Act\n        var result = _monitor.Get(configName);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(_testConfiguration, result);\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void Get_WithEmptyName_ShouldReturnConfigurationFromRepository()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n\n        // Act\n        var result = _monitor.Get(string.Empty);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(_testConfiguration, result);\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void Get_WithNullName_ShouldReturnConfigurationFromRepository()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n\n        // Act\n        var result = _monitor.Get(null);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(_testConfiguration, result);\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void OnChange_WithValidListener_ShouldRegisterCallbackOnChangeToken()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        var callbackInvoked = false;\n        void listener(IInternalConfiguration config, string name)\n        {\n            callbackInvoked = true;\n        }\n\n        // Act\n        var disposable = _monitor.OnChange(listener);\n\n        // Assert\n        Assert.NotNull(disposable);\n        _mChangeToken.Verify(\n            x => x.RegisterChangeCallback(It.IsAny<Action<object>>(), It.IsAny<object>()),\n            Times.Once\n        );\n        Assert.False(callbackInvoked);\n    }\n\n    [Fact]\n    public void OnChange_ShouldReturnDisposableWrapper()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        var mockDisposable = new Mock<IDisposable>();\n        _mChangeToken\n            .Setup(x => x.RegisterChangeCallback(It.IsAny<Action<object>>(), It.IsAny<object>()))\n            .Returns(mockDisposable.Object);\n\n        // Act\n        var result = _monitor.OnChange((config, name) => { });\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(mockDisposable.Object, result);\n    }\n\n    [Fact]\n    public void OnChange_WhenChangeTokenCallbackIsInvoked_ShouldCallListenerWithCurrentValueAndEmptyString()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        IInternalConfiguration capturedConfig = null;\n        string capturedName = null;\n        void listener(IInternalConfiguration config, string name)\n        {\n            capturedConfig = config;\n            capturedName = name;\n        }\n\n        Action<object> capturedCallback = null;\n        _mChangeToken\n            .Setup(x => x.RegisterChangeCallback(It.IsAny<Action<object>>(), It.IsAny<object>()))\n            .Callback<Action<object>, object>((callback, state) => capturedCallback = callback)\n            .Returns(new Mock<IDisposable>().Object);\n\n        // Act\n        _monitor.OnChange(listener);\n        capturedCallback?.Invoke(null);\n\n        // Assert\n        Assert.NotNull(capturedConfig);\n        Assert.Same(_testConfiguration, capturedConfig);\n        Assert.Equal(string.Empty, capturedName);\n    }\n\n    [Fact]\n    public void OnChange_WithNullListener_ShouldThrowArgumentNullException()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        Action<IInternalConfiguration, string> listener = null;\n\n        // Act & Assert\n        var e = Assert.Throws<ArgumentNullException>(\n            () => _monitor.OnChange(listener));\n\n        // Assert\n        Assert.NotNull(e);\n        Assert.Equal(nameof(listener), e.ParamName);\n    }\n\n    [Fact]\n    public void CurrentValue_MultipleInvocations_ShouldCallRepositoryEachTime()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n\n        // Act\n        _ = _monitor.CurrentValue;\n        _ = _monitor.CurrentValue;\n        _ = _monitor.CurrentValue;\n\n        // Assert\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Exactly(3));\n    }\n\n    [Fact]\n    public void Get_MultipleInvocationsWithDifferentNames_ShouldCallRepositoryEachTime()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n\n        // Act\n        _ = _monitor.Get(\"config1\");\n        _ = _monitor.Get(\"config2\");\n        _ = _monitor.Get(\"config3\");\n\n        // Assert\n        _mConfigurationRepo.Verify(x => x.Get(), Times.Exactly(3));\n    }\n\n    [Fact]\n    public void OnChange_MultipleListenersRegistered_ShouldRegisterAllCallbacks()\n    {\n        // Arrange\n        GivenATestConfigurationIsSet();\n        GivenTheMonitorIsCreated();\n        var listener1Called = false;\n        var listener2Called = false;\n\n        // Act\n        _monitor.OnChange((config, name) => listener1Called = true);\n        _monitor.OnChange((config, name) => listener2Called = true);\n\n        // Assert\n        _mChangeToken.Verify(\n            x => x.RegisterChangeCallback(It.IsAny<Action<object>>(), It.IsAny<object>()),\n            Times.Exactly(2));\n        Assert.False(listener1Called);\n        Assert.False(listener2Called);\n    }\n\n    // Helper methods\n    private void GivenTheRepositoryMock()\n    {\n        _mConfigurationRepo = new Mock<IInternalConfigurationRepository>();\n    }\n\n    private void GivenTheChangeTokenSourceMock()\n    {\n        _mChangeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>();\n        _mChangeTokenSource.Setup(x => x.ChangeToken).Returns(_mChangeToken?.Object ?? new Mock<IChangeToken>().Object);\n    }\n\n    private void GivenTheChangeTokenMock()\n    {\n        _mChangeToken = new Mock<IChangeToken>();\n        _mChangeToken\n            .Setup(x => x.RegisterChangeCallback(It.IsAny<Action<object>>(), It.IsAny<object>()))\n            .Returns(new Mock<IDisposable>().Object);\n    }\n\n    private void GivenATestConfigurationIsSet()\n    {\n        _testConfiguration = new Mock<IInternalConfiguration>().Object;\n        _repoResponse = new OkResponse<IInternalConfiguration>(_testConfiguration);\n        _mConfigurationRepo.Setup(x => x.Get()).Returns(_repoResponse);\n    }\n\n    private void GivenTheMonitorIsCreated()\n    {\n        _mChangeTokenSource.Setup(x => x.ChangeToken).Returns(_mChangeToken.Object);\n        _monitor = new OcelotConfigurationMonitor(_mConfigurationRepo.Object, _mChangeTokenSource.Object);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs",
    "content": "﻿using Ocelot.Configuration;\r\nusing Ocelot.Configuration.Parser;\r\nusing Ocelot.Errors;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\n/// <summary>\r\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-headers\">Claims to Headers</see>.\r\n/// </summary>\r\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\r\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\r\npublic class ClaimToThingConfigurationParserTests : UnitTest\r\n{\r\n    private readonly ClaimToThingConfigurationParser _parser;\r\n\r\n    public ClaimToThingConfigurationParserTests()\r\n    {\r\n        _parser = new ClaimToThingConfigurationParser();\r\n    }\r\n\r\n    [Fact]\r\n    public void Returns_no_instructions_error()\r\n    {\r\n        // Arrange\r\n        var dictionary = new Dictionary<string, string>\r\n        {\r\n            {\"CustomerId\", string.Empty},\r\n        };\r\n\r\n        // Act\r\n        var result = WhenICallTheExtractor(dictionary);\r\n\r\n        // Assert\r\n        ThenAnErrorIsReturned(result, new ErrorResponse<ClaimToThing>(\r\n            new List<Error>\r\n            {\r\n                new NoInstructionsError(\">\"),\r\n            }));\r\n    }\r\n\r\n    [Fact]\r\n    public void Returns_no_instructions_not_for_claims_error()\r\n    {\r\n        // Arrange\r\n        var dictionary = new Dictionary<string, string>\r\n        {\r\n            {\"CustomerId\", \"Cheese[CustomerId] > value\"},\r\n        };\r\n\r\n        // Act\r\n        var result = WhenICallTheExtractor(dictionary);\r\n\r\n        // Assert\r\n        ThenAnErrorIsReturned(result, new ErrorResponse<ClaimToThing>(new List<Error>\r\n            {\r\n                new InstructionNotForClaimsError(),\r\n            }));\r\n    }\r\n\r\n    [Fact]\r\n    public void Can_parse_entry_to_work_out_properties_with_key()\r\n    {\r\n        // Arrange\r\n        var dictionary = new Dictionary<string, string>\r\n        {\r\n            {\"CustomerId\", \"Claims[CustomerId] > value\"},\r\n        };\r\n\r\n        // Act\r\n        var result = WhenICallTheExtractor(dictionary);\r\n\r\n        // Assert\r\n        ThenTheClaimParserPropertiesAreReturned(result,\r\n            new OkResponse<ClaimToThing>(new ClaimToThing(\"CustomerId\", \"CustomerId\", string.Empty, 0)));\r\n    }\r\n\r\n    [Fact]\r\n    public void Can_parse_entry_to_work_out_properties_with_key_delimiter_and_index()\r\n    {\r\n        // Arrange\r\n        var dictionary = new Dictionary<string, string>\r\n        {\r\n            {\"UserId\", \"Claims[Subject] > value[0] > |\"},\r\n        };\r\n\r\n        // Act\r\n        var result = WhenICallTheExtractor(dictionary);\r\n\r\n        // Assert\r\n        ThenTheClaimParserPropertiesAreReturned(result,\r\n            new OkResponse<ClaimToThing>(new ClaimToThing(\"UserId\", \"Subject\", \"|\", 0)));\r\n    }\r\n\r\n    private static void ThenAnErrorIsReturned(Response<ClaimToThing> result, Response<ClaimToThing> expected)\r\n    {\r\n        result.IsError.ShouldBe(expected.IsError);\r\n        result.Errors[0].ShouldBeOfType(expected.Errors[0].GetType());\r\n    }\r\n\r\n    private static void ThenTheClaimParserPropertiesAreReturned(Response<ClaimToThing> result, Response<ClaimToThing> expected)\r\n    {\r\n        result.Data.NewKey.ShouldBe(expected.Data.NewKey);\r\n        result.Data.Delimiter.ShouldBe(expected.Data.Delimiter);\r\n        result.Data.Index.ShouldBe(expected.Data.Index);\r\n        result.IsError.ShouldBe(expected.IsError);\r\n    }\r\n\r\n    private Response<ClaimToThing> WhenICallTheExtractor(Dictionary<string, string> dictionary)\r\n    {\r\n        var first = dictionary.First();\r\n        return _parser.Extract(first.Key, first.Value);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs",
    "content": "using Ocelot.Configuration;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.Parser;\r\nusing Ocelot.Errors;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class ClaimsToThingCreatorTests : UnitTest\r\n{\r\n    private readonly Mock<IClaimToThingConfigurationParser> _configParser;\r\n    private Dictionary<string, string> _claimsToThings;\r\n    private readonly ClaimsToThingCreator _claimsToThingsCreator;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n    private List<ClaimToThing> _result;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n\r\n    public ClaimsToThingCreatorTests()\r\n    {\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _loggerFactory\r\n            .Setup(x => x.CreateLogger<ClaimsToThingCreator>())\r\n            .Returns(_logger.Object);\r\n        _configParser = new Mock<IClaimToThingConfigurationParser>();\r\n        _claimsToThingsCreator = new ClaimsToThingCreator(_configParser.Object, _loggerFactory.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_claims_to_things()\r\n    {\r\n        // Arrange\r\n        var userInput = new Dictionary<string, string>\r\n        {\r\n            {\"CustomerId\", \"Claims[CustomerId] > value\"},\r\n        };\r\n        var claimsToThing = new OkResponse<ClaimToThing>(new ClaimToThing(\"CustomerId\", \"CustomerId\", string.Empty, 0));\r\n        GivenTheFollowingDictionary(userInput);\r\n        GivenTheConfigHeaderExtractorReturns(claimsToThing);\r\n\r\n        // Act\r\n        WhenIGetTheThings();\r\n\r\n        // Assert\r\n        ThenTheConfigParserIsCalledCorrectly();\r\n        ThenClaimsToThingsAreReturned();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_log_error_if_cannot_parse_claim_to_thing()\r\n    {\n        // Arrange\r\n        var userInput = new Dictionary<string, string>\r\n        {\r\n            {\"CustomerId\", \"Claims[CustomerId] > value\"},\r\n        };\r\n        var claimsToThing = new ErrorResponse<ClaimToThing>(It.IsAny<Error>());\n        GivenTheFollowingDictionary(userInput);\r\n        GivenTheConfigHeaderExtractorReturns(claimsToThing);\r\n\r\n        // Act\r\n        WhenIGetTheThings();\r\n\r\n        // Assert\r\n        ThenTheConfigParserIsCalledCorrectly();\r\n        ThenNoClaimsToThingsAreReturned();\r\n    }\r\n\r\n    private void ThenClaimsToThingsAreReturned()\r\n    {\r\n        _result.Count.ShouldBeGreaterThan(0);\r\n    }\r\n\r\n    private void GivenTheFollowingDictionary(Dictionary<string, string> claimsToThings)\r\n    {\r\n        _claimsToThings = claimsToThings;\r\n    }\r\n\r\n    private void GivenTheConfigHeaderExtractorReturns(Response<ClaimToThing> expected)\r\n    {\r\n        _configParser\r\n            .Setup(x => x.Extract(It.IsAny<string>(), It.IsAny<string>()))\r\n            .Returns(expected);\r\n    }\r\n\r\n    private void ThenNoClaimsToThingsAreReturned()\r\n    {\r\n        _result.Count.ShouldBe(0);\r\n    }\r\n\r\n    private void WhenIGetTheThings()\r\n    {\r\n        _result = _claimsToThingsCreator.Create(_claimsToThings);\r\n    }\r\n\r\n    private void ThenTheConfigParserIsCalledCorrectly()\r\n    {\r\n        _configParser\r\n            .Verify(x => x.Extract(_claimsToThings.First().Key, _claimsToThings.First().Value), Times.Once);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Administration;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Runtime.CompilerServices;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class ConfigurationCreatorTests : UnitTest\r\n{\r\n    private ConfigurationCreator _creator;\r\n    private InternalConfiguration _result;\r\n    private readonly Mock<IServiceProviderConfigurationCreator> _spcCreator;\r\n    private readonly Mock<IQoSOptionsCreator> _qosCreator;\r\n    private readonly Mock<IHttpHandlerOptionsCreator> _hhoCreator;\r\n    private readonly Mock<ILoadBalancerOptionsCreator> _lboCreator;\r\n    private readonly Mock<IVersionCreator> _vCreator;\r\n    private readonly Mock<IVersionPolicyCreator> _vpCreator;\r\n    private readonly Mock<IMetadataCreator> _mdCreator;\r\n    private readonly Mock<IRateLimitOptionsCreator> _rlCreator;\r\n    private readonly Mock<ICacheOptionsCreator> _coCreator;\r\n    private readonly Mock<IAuthenticationOptionsCreator> _authCreator;\n    private FileConfiguration _fileConfig;\r\n    private Route[] _routes;\r\n    private ServiceProviderConfiguration _spc;\r\n    private LoadBalancerOptions _lbo;\r\n    private QoSOptions _qoso;\r\n    private HttpHandlerOptions _hho;\r\n    private CacheOptions _co;\r\n    private AuthenticationOptions _ao;\r\n    private AdministrationPath _adminPath;\r\n    private readonly ServiceCollection _serviceCollection;\r\n\r\n    public ConfigurationCreatorTests()\r\n    {\r\n        _vCreator = new Mock<IVersionCreator>();\r\n        _vpCreator = new Mock<IVersionPolicyCreator>();\n        _lboCreator = new Mock<ILoadBalancerOptionsCreator>();\r\n        _hhoCreator = new Mock<IHttpHandlerOptionsCreator>();\r\n        _qosCreator = new Mock<IQoSOptionsCreator>();\r\n        _spcCreator = new Mock<IServiceProviderConfigurationCreator>();\r\n        _mdCreator = new Mock<IMetadataCreator>();\r\n        _rlCreator = new Mock<IRateLimitOptionsCreator>();\r\n        _coCreator = new Mock<ICacheOptionsCreator>();\r\n        _authCreator = new Mock<IAuthenticationOptionsCreator>();\r\n        _serviceCollection = new ServiceCollection();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_build_configuration_with_no_admin_path()\r\n    {\r\n        // Arrange\r\n        GivenTheDependenciesAreSetUp();\r\n\r\n        // Act\r\n        WhenICreate();\r\n\r\n        // Assert\r\n        ThenTheDepdenciesAreCalledCorrectly();\r\n        ThenThePropertiesAreSetCorrectly();\r\n        _result.AdministrationPath.ShouldBeNull();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_build_configuration_with_admin_path()\r\n    {\r\n        // Arrange\r\n        GivenTheDependenciesAreSetUp();\r\n        GivenTheAdminPath();\r\n\r\n        // Act\r\n        WhenICreate();\r\n\r\n        // Assert\r\n        ThenTheDepdenciesAreCalledCorrectly();\r\n        ThenThePropertiesAreSetCorrectly();\r\n        ThenTheAdminPathIsSet();\r\n    }\r\n\r\n    [Fact]\r\n    public void Configuration_GlobalConfiguration_SoftNullGuard()\r\n    {\r\n        // Arrange\r\n        GivenTheDependenciesAreSetUp();\r\n        _fileConfig.GlobalConfiguration = null;\r\n\r\n        // Act\r\n        WhenICreate();\r\n\r\n        // Assert\r\n        ThenTheDepdenciesAreCalledCorrectly();\r\n        ThenThePropertiesAreSetCorrectly();\r\n    }\r\n\r\n    private void ThenThePropertiesAreSetCorrectly()\r\n    {\r\n        _fileConfig.GlobalConfiguration ??= new();\r\n        _result.ShouldNotBeNull();\r\n        _result.ServiceProviderConfiguration.ShouldBe(_spc);\r\n        _result.LoadBalancerOptions.ShouldBe(_lbo);\r\n        _result.QoSOptions.ShouldBe(_qoso);\r\n        _result.HttpHandlerOptions.ShouldBe(_hho);\r\n        _result.CacheOptions.ShouldBe(_co);\r\n        _result.AuthenticationOptions.ShouldBe(_ao);\r\n        _result.Routes.ShouldBe(_routes);\r\n        _result.RequestId.ShouldBe(_fileConfig.GlobalConfiguration.RequestIdKey);\r\n        _result.DownstreamScheme.ShouldBe(_fileConfig.GlobalConfiguration.DownstreamScheme);\r\n    }\r\n\r\n    private void ThenTheAdminPathIsSet()\r\n    {\r\n        _result.AdministrationPath.ShouldBe(\"wooty\");\r\n    }\r\n\r\n    private void ThenTheDepdenciesAreCalledCorrectly()\r\n    {\r\n        _spcCreator.Verify(x => x.Create(It.IsAny<FileGlobalConfiguration>()), Times.Once);\r\n        _lboCreator.Verify(x => x.Create(It.IsAny<FileLoadBalancerOptions>()), Times.Once);\r\n        _qosCreator.Verify(x => x.Create(It.IsAny<FileQoSOptions>()), Times.Once);\r\n        _hhoCreator.Verify(x => x.Create(It.IsAny<FileHttpHandlerOptions>()), Times.Once);\r\n        _vCreator.Verify(x => x.Create(It.IsAny<string>()), Times.Once);\r\n        _vpCreator.Verify(x => x.Create(It.IsAny<string>()), Times.Once);\r\n        _mdCreator.Verify(x => x.Create(It.IsAny<IDictionary<string, string>>(), It.IsAny<FileGlobalConfiguration>()), Times.Once);\r\n        _vCreator.Verify(x => x.Create(It.IsAny<string>()), Times.Once);\r\n        _rlCreator.Verify(x => x.Create(It.IsAny<FileGlobalConfiguration>()), Times.Once);\r\n    }\r\n\r\n    private void GivenTheAdminPath([CallerMemberName] string testName = nameof(ConfigurationCreatorTests))\r\n    {\r\n        _adminPath = new AdministrationPath(\"wooty\", testName);\r\n        _serviceCollection.AddSingleton<IAdministrationPath>(_adminPath);\r\n    }\r\n\r\n    private void GivenTheDependenciesAreSetUp()\r\n    {\r\n        _fileConfig = new FileConfiguration\r\n        {\r\n            GlobalConfiguration = new FileGlobalConfiguration(),\r\n        };\r\n        _routes = Array.Empty<Route>();\r\n        _spc = new ServiceProviderConfiguration();\r\n        _lbo = new();\r\n        _qoso = new QoSOptions();\r\n        _hho = new HttpHandlerOptions();\r\n        _co = new(new(), \"region\");\r\n        _ao = new();\r\n        _spcCreator.Setup(x => x.Create(It.IsAny<FileGlobalConfiguration>())).Returns(_spc);\r\n        _lboCreator.Setup(x => x.Create(It.IsAny<FileLoadBalancerOptions>())).Returns(_lbo);\r\n        _qosCreator.Setup(x => x.Create(It.IsAny<FileQoSOptions>())).Returns(_qoso);\r\n        _hhoCreator.Setup(x => x.Create(It.IsAny<FileHttpHandlerOptions>())).Returns(_hho);\r\n        _coCreator.Setup(x => x.Create(It.IsAny<FileCacheOptions>())).Returns(_co);\r\n        _authCreator.Setup(x => x.Create(It.IsAny<FileAuthenticationOptions>())).Returns(_ao);\r\n    }\r\n\r\n    private void WhenICreate()\r\n    {\r\n        var serviceProvider = _serviceCollection.BuildServiceProvider(true);\r\n        _creator = new ConfigurationCreator(serviceProvider,\r\n            _authCreator.Object,\r\n            _spcCreator.Object,\r\n            _qosCreator.Object,\r\n            _hhoCreator.Object,\r\n            _lboCreator.Object,\r\n            _vCreator.Object,\r\n            _vpCreator.Object,\r\n            _mdCreator.Object,\r\n            _rlCreator.Object,\r\n            _coCreator.Object);\r\n        _result = _creator.Create(_fileConfig, _routes);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\n[Trait(\"Feat\", \"738\")]\npublic class DefaultMetadataCreatorTests : UnitTest\n{\n    private readonly DefaultMetadataCreator _sut = new();\n    private static readonly Dictionary<string, string> Empty = new();\n\n    [Fact]\n    public void Should_return_empty_metadata()\n    {\n        // Arrange, Act\n        var result = _sut.Create(Empty, new());\n\n        // Assert\n        result.Metadata.Keys.ShouldBeEmpty();\n    }\n\n    [Fact]\n    public void Should_return_global_metadata()\n    {\n        // Arrange\n        var global = GivenSomeMetadataInGlobalConfiguration();\n\n        // Act\n        var result = _sut.Create(Empty, global);\n\n        // Assert\n        ThenDownstreamMetadataMustContain(result, \"foo\", \"bar\");\n    }\n\n    [Fact]\n    public void Should_return_route_metadata()\n    {\n        // Arrange\n        var metadata = GivenSomeMetadataInRoute();\n\n        // Act\n        var result = _sut.Create(metadata, new());\n\n        // Assert\n        ThenDownstreamMetadataMustContain(result, \"foo\", \"baz\");\n    }\n\n    [Fact]\n    public void Should_overwrite_global_metadata()\n    {\n        // Arrange\n        var global = GivenSomeMetadataInGlobalConfiguration();\n        var metadata = GivenSomeMetadataInRoute();\n\n        // Act\n        var result = _sut.Create(metadata, global);\n\n        // Assert\n        ThenDownstreamMetadataMustContain(result, \"foo\", \"baz\");\n    }\n\n    private static FileGlobalConfiguration GivenSomeMetadataInGlobalConfiguration() => new()\n    {\n        Metadata = new Dictionary<string, string>\n        {\n            [\"foo\"] = \"bar\",\n        },\n    };\n\n    private static Dictionary<string, string> GivenSomeMetadataInRoute() => new()\n    {\n        [\"foo\"] = \"baz\",\n    };\n\n    private static void ThenDownstreamMetadataMustContain(MetadataOptions result, string key, string value)\n    {\n        result.Metadata.Keys.ShouldContain(key);\n        result.Metadata[key].ShouldBeEquivalentTo(value);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class DownstreamAddressesCreatorTests : UnitTest\n{\n    public readonly DownstreamAddressesCreator _creator;\n\n    public DownstreamAddressesCreatorTests()\n    {\n        _creator = new DownstreamAddressesCreator();\n    }\n\n    [Fact]\n    public void Should_do_nothing()\n    {\n        // Arrange\n        var route = new FileRoute();\n        var expected = new List<DownstreamHostAndPort>();\n\n        // Act\n        var result = _creator.Create(route);\n\n        // Assert\n        result.TheThenFollowingIsReturned(expected);\n    }\n\n    [Fact]\n    public void Should_create_downstream_addresses_from_old_downstream_path_and_port()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            DownstreamHostAndPorts = new List<FileHostAndPort>\n            {\n                new(\"test\", 80),\n            },\n        };\n        var expected = new List<DownstreamHostAndPort>\n        {\n            new(\"test\", 80),\n        };\n\n        // Act\n        var result = _creator.Create(route);\n\n        // Assert\n        result.TheThenFollowingIsReturned(expected);\n    }\n\n    [Fact]\n    public void Should_create_downstream_addresses_from_downstream_host_and_ports()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            DownstreamHostAndPorts = new List<FileHostAndPort>\n            {\n                new(\"test\", 80),\n                new(\"west\", 443),\n            },\n        };\n        var expected = new List<DownstreamHostAndPort>\n        {\n            new(\"test\", 80),\n            new(\"west\", 443),\n        };\n\n        // Act\n        var result = _creator.Create(route);\n\n        // Assert\n        result.TheThenFollowingIsReturned(expected);\n    }\n}\n\ninternal static class ListOfDownstreamHostAndPortExtensions\n{\n    public static void TheThenFollowingIsReturned(this List<DownstreamHostAndPort> actual, List<DownstreamHostAndPort> expecteds)\n    {\n        actual.Count.ShouldBe(expecteds.Count);\n\n        for (var i = 0; i < actual.Count; i++)\n        {\n            var result = actual[i];\n            var expected = expecteds[i];\n\n            result.Host.ShouldBe(expected.Host);\n            result.Port.ShouldBe(expected.Port);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Metadata;\nusing Ocelot.Values;\nusing System.Text.Json;\n\nnamespace Ocelot.UnitTests.Configuration;\n\n[Trait(\"Feat\", \"738\")]\npublic class DownstreamRouteExtensionsTests\n{\n    private readonly DownstreamRoute _downstreamRoute;\n\n    public DownstreamRouteExtensionsTests()\n    {\n        _downstreamRoute = new DownstreamRoute(\n            null,\n            new UpstreamPathTemplate(null, 0, false, null),\n            new List<HeaderFindAndReplace>(),\n            new List<HeaderFindAndReplace>(),\n            new List<DownstreamHostAndPort>(),\n            null,\n            null,\n            new HttpHandlerOptions(),\n            new QoSOptions(),\n            null,\n            null,\n            new CacheOptions(0, null, null, null),\n            new LoadBalancerOptions(null, null, 0),\n            new RateLimitOptions(false),\n            new Dictionary<string, string>(),\n            new List<ClaimToThing>(),\n            new List<ClaimToThing>(),\n            new List<ClaimToThing>(),\n            new List<ClaimToThing>(),\n            new AuthenticationOptions(null, null),\n            new DownstreamPathTemplate(null),\n            null,\n            new List<string>(),\n            new List<AddHeader>(),\n            new List<AddHeader>(),\n            default,\n            new SecurityOptions(),\n            null,\n            new Version(),\n            HttpVersionPolicy.RequestVersionExact,\n            new(),\n            new MetadataOptions(new FileMetadataOptions()),\n            0);\n    }\n\n    [Theory]\n    [InlineData(\"key1\", null)]\n    [InlineData(\"hello\", \"world\")]\n    public void Should_return_default_value_when_key_not_found(string key, string defaultValue)\n    {\n        // Arrange\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, defaultValue);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata(key, defaultValue);\n\n        // Assert\n        metadataValue.ShouldBe(defaultValue);\n    }\n\n    [Theory]\n    [InlineData(\"hello\", \"world\")]\n    [InlineData(\"object.key\", \"value1,value2,value3\")]\n    public void Should_return_found_metadata_value(string key, string value)\n    {\n        // Arrange\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<string>(key);\n\n        //Assert\n        metadataValue.ShouldBe(value);\n    }\n\n    [Theory]\n    [InlineData(\"mykey\", \"\")]\n    [InlineData(\"mykey\", \"value1\", \"value1\")]\n    [InlineData(\"mykey\", \"value1,value2\", \"value1\", \"value2\")]\n    [InlineData(\"mykey\", \"value1, value2\", \"value1\", \"value2\")]\n    [InlineData(\"mykey\", \"value1,,,value2\", \"value1\", \"value2\")]\n    [InlineData(\"mykey\", \"value1,   ,value2\", \"value1\", \"value2\")]\n    [InlineData(\"mykey\", \"value1, value2, value3\", \"value1\", \"value2\", \"value3\")]\n    [InlineData(\"mykey\", \",  ,value1,  ,,  ,,,,,value2,,,   \", \"value1\", \"value2\")]\n    public void Should_split_strings(string key, string value, params string[] expected)\n    {\n        // Arrange\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<string[]>(key);\n\n        //Assert\n        metadataValue.ShouldBe(expected);\n    }\n\n    [Fact]\n    public void Should_parse_from_json_null() => Should_parse_object_from_json<object>(\"mykey\", \"null\", null);\n\n    [Fact]\n    public void Should_parse_from_json_string() => Should_parse_object_from_json<string>(\"mykey\", \"string\", \"string\");\n\n    [Fact]\n    public void Should_parse_from_json_numbers() => Should_parse_object_from_json<int>(\"mykey\", \"123\", 123);\n\n    [Fact]\n    public void Should_parse_from_object()\n        => Should_parse_object_from_json<FakeObject>(\n            \"mykey\",\n            \"{\\\"Id\\\": 88, \\\"Value\\\": \\\"Hello World!\\\", \\\"MyTime\\\": \\\"2024-01-01T10:10:10.000Z\\\"}\",\n            new FakeObject { Id = 88, Value = \"Hello World!\", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) });\n\n    private void Should_parse_object_from_json<T>(string key, string value, object expected)\n    {\n        // Arrange\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<T>(key);\n\n        //Assert\n        metadataValue.ShouldBeEquivalentTo(expected);\n    }\n\n    [Fact]\n    public void Should_parse_from_json_array()\n    {\n        // Arrange\n        var key = \"mykey\";\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, \"[\\\"value1\\\", \\\"value2\\\", \\\"value3\\\"]\");\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<IEnumerable<string>>(key);\n\n        //Assert\n        IEnumerable<string> enumerable = metadataValue as string[] ?? metadataValue.ToArray();\n        enumerable.ShouldNotBeNull();\n        enumerable.ElementAt(0).ShouldBe(\"value1\");\n        enumerable.ElementAt(1).ShouldBe(\"value2\");\n        enumerable.ElementAt(2).ShouldBe(\"value3\");\n    }\n\n    [Fact]\n    public void Should_throw_error_when_invalid_json()\n    {\n        // Arrange\n        var key = \"mykey\";\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, \"[[[\");\n\n        // Act\n\n        //Assert\n        Assert.Throws<JsonException>(() => _ = _downstreamRoute.GetMetadata<IEnumerable<string>>(key));\n    }\n\n    [Fact]\n    public void Should_parse_json_with_custom_json_settings_options()\n    {\n        // Arrange\n        var key = \"mykey\";\n        var value = \"{\\\"id\\\": 88, \\\"value\\\": \\\"Hello World!\\\", \\\"myTime\\\": \\\"2024-01-01T10:10:10.000Z\\\"}\";\n        var expected = new FakeObject\n        {\n            Id = 88,\n            Value = \"Hello World!\",\n            MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified),\n        };\n        var serializerOptions = new JsonSerializerOptions()\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n        };\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<FakeObject>(key, options: serializerOptions);\n\n        //Assert\n        metadataValue.ShouldBeEquivalentTo(expected);\n    }\n\n    record FakeObject\n    {\n        public int Id { get; set; }\n        public string Value { get; set; }\n        public DateTime MyTime { get; set; }\n    }\n\n    [Theory]\n    [InlineData(\"0\", 0)]\n    [InlineData(\"99\", 99)]\n    [InlineData(\"500\", 500)]\n    [InlineData(\"999999999\", 999999999)]\n    public void Should_parse_integers(string value, int expected) => Should_parse_number(value, expected);\n\n    [Theory]\n    [InlineData(\"0\", 0)]\n    [InlineData(\"0.5\", 0.5)]\n    [InlineData(\"99\", 99)]\n    [InlineData(\"99.5\", 99.5)]\n    [InlineData(\"999999999\", 999999999)]\n    [InlineData(\"999999999.5\", 999999999.5)]\n    public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected);\n\n    private void Should_parse_number<T>(string value, T expected)\n    {\n        // Arrange\n        var key = \"mykey\";\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var metadataValue = _downstreamRoute.GetMetadata<T>(key);\n\n        //Assert\n        metadataValue.ShouldBe(expected);\n    }\n\n    [Fact]\n    public void Should_throw_error_when_invalid_number()\n    {\n        // Arrange\n        var key = \"mykey\";\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, \"xyz\");\n\n        // Act\n\n        // Assert\n        Assert.Throws<FormatException>(() => _ = _downstreamRoute.GetMetadata<int>(key));\n    }\n\n    [Theory]\n    [InlineData(\"true\", true)]\n    [InlineData(\"yes\", true)]\n    [InlineData(\"on\", true)]\n    [InlineData(\"enabled\", true)]\n    [InlineData(\"enable\", true)]\n    [InlineData(\"ok\", true)]\n    [InlineData(\"  true  \", true)]\n    [InlineData(\"  yes  \", true)]\n    [InlineData(\"  on  \", true)]\n    [InlineData(\"  enabled  \", true)]\n    [InlineData(\"  enable  \", true)]\n    [InlineData(\"  ok  \", true)]\n    [InlineData(\"\", false)]\n    [InlineData(\"  \", false)]\n    [InlineData(null, false)]\n    [InlineData(\"false\", false)]\n    [InlineData(\"off\", false)]\n    [InlineData(\"disabled\", false)]\n    [InlineData(\"disable\", false)]\n    [InlineData(\"no\", false)]\n    [InlineData(\"abcxyz\", false)]\n    public void Should_parse_truthy_values(string value, bool expected)\n    {\n        // Arrange\n        var key = \"mykey\";\n        _downstreamRoute.MetadataOptions.Metadata.Add(key, value);\n\n        // Act\n        var isTrusthy = _downstreamRoute.GetMetadata<bool>(key);\n\n        //Assert\n        isTrusthy.ShouldBe(expected);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/DownstreamRouteTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\n\nnamespace Ocelot.UnitTests.Configuration;\n\n[Collection(nameof(SequentialTests))]\npublic class DownstreamRouteTests\n{\n    [Fact]\n    public void Name_UnknownPaths_ShouldBeQuestionMark()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(null)\n            .WithDownstreamPathTemplate(null)\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(\"?\", actual);\n    }\n\n    [Theory]\n    [InlineData(null, null, \"?\")]\n    [InlineData(null, \"NoServiceDiscoveryDownstreamPath\", \"NoServiceDiscoveryDownstreamPath\")]\n    [InlineData(\"NoServiceDiscoveryUpstreamPath\", null, \"NoServiceDiscoveryUpstreamPath\")]\n    public void Name_NoServiceDiscovery_ShouldBePathTemplate(string upstreamPathTemplate, string downstreamPathTemplate, string expectedName)\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(upstreamPathTemplate, 0, false, upstreamPathTemplate))\n            .WithDownstreamPathTemplate(downstreamPathTemplate)\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(expectedName, actual);\n    }\n\n    [Theory]\n    [InlineData(null, \"?\")]\n    [InlineData(\"\", \"?\")]\n    [InlineData(\"TestTemplate\", \"TestTemplate\")]\n    public void Name_UpstreamPathTemplate_ShouldContainOriginalValue(string upstreamPathTemplate, string expectedName)\n    {\n        // Arrange\n        var template = new UpstreamPathTemplateBuilder()\n            .WithTemplate(upstreamPathTemplate)\n            .WithOriginalValue(upstreamPathTemplate)\n            .Build();\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(template)\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(expectedName, actual);\n    }\n\n    [Theory]\n    [InlineData(null, \"?\")]\n    [InlineData(\"\", \"?\")]\n    [InlineData(\"TestTemplate\", \"TestTemplate\")]\n    public void Name_DownstreamPathTemplate_ShouldContainPathTemplate(string downstreamPathTemplate, string expectedName)\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(null)\n            .WithDownstreamPathTemplate(downstreamPathTemplate)\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(expectedName, actual);\n    }\n\n    [Fact]\n    public void Name_WithServiceDiscovery_ShouldBeUniqueDiscoveryString()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithServiceName(\"TestService\")\n            .WithServiceNamespace(\"TestNamespace\")\n            .WithUpstreamPathTemplate(new(\"/UpstreamPath\", 0, false, \"/UpstreamPath\"))\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(\"TestNamespace:TestService:/UpstreamPath\", actual);\n    }\n\n    [Theory]\n    [InlineData(false, \"/test\")]\n    [InlineData(true, \"TestNamespace:TestService:/test\")]\n    public void Name_UseServiceDiscovery_ShouldContainUpstreamPathTemplate(bool useServiceDiscovery, string expectedName)\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/test\", 0, false, \"/test\"))\n            .WithServiceName(useServiceDiscovery ? \"TestService\" : string.Empty)\n            .WithServiceNamespace(\"TestNamespace\")\n            .Build();\n\n        // Act\n        var actual = route.Name();\n\n        // Assert\n        Assert.Equal(expectedName, actual);\n        Assert.Contains(\"/test\", actual);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0, DownstreamRoute.DefTimeout)] // not in range\n    [InlineData(DownstreamRoute.LowTimeout - 1, DownstreamRoute.DefTimeout)] // not in range\n    [InlineData(DownstreamRoute.LowTimeout, DownstreamRoute.LowTimeout)] // in range\n    [InlineData(DownstreamRoute.LowTimeout + 1, DownstreamRoute.LowTimeout + 1)] // in range\n    [InlineData(DownstreamRoute.DefTimeout, DownstreamRoute.DefTimeout)] // in range\n    public void DefaultTimeoutSeconds_Setter_ShouldBeGreaterThanOrEqualToThree(int value, int expected)\n    {\n        // Arrange, Act\n        DownstreamRoute.DefaultTimeoutSeconds = value;\n\n        // Assert\n        Assert.Equal(expected, DownstreamRoute.DefaultTimeoutSeconds);\n        DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; // recover clean state after assembly starting\n    }\n\n    [Fact]\n    public void ToString_ShouldBeLoadBalancerKey()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(\"testLbKey\")\n            .Build();\n\n        // Act\n        var actual = route.ToString();\n\n        // Assert\n        Assert.Equal(\"testLbKey\", actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/DynamicRoutesCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class DynamicRoutesCreatorTests : UnitTest\n{\n    private readonly DynamicRoutesCreator _creator;\n    private readonly Mock<IRouteKeyCreator> _lbKeyCreator = new();\n    private readonly Mock<IHttpHandlerOptionsCreator> _hhoCreator = new();\n    private readonly Mock<ILoadBalancerOptionsCreator> _lboCreator = new();\n    private readonly Mock<IQoSOptionsCreator> _qosCreator = new();\n    private readonly Mock<IRateLimitOptionsCreator> _rloCreator = new();\n    private readonly Mock<IVersionCreator> _versionCreator = new();\n    private readonly Mock<IVersionPolicyCreator> _versionPolicyCreator = new();\n    private readonly Mock<IMetadataCreator> _metadataCreator = new();\n    private readonly Mock<ICacheOptionsCreator> _cacheCreator = new();\n    private readonly Mock<IAuthenticationOptionsCreator> _authCreator = new();\n    private IReadOnlyList<Route> _result;\n    private FileConfiguration _fileConfig;\n    private RateLimitOptions[] _rlo;\n    private Version _version;\n    private HttpVersionPolicy _versionPolicy;\n    private Dictionary<string, string> _expectedMetadata;\n\n    public DynamicRoutesCreatorTests()\n    {\n        _creator = new DynamicRoutesCreator(\n            _authCreator.Object,\n            _cacheCreator.Object,\n            _hhoCreator.Object,\n            _lboCreator.Object,\n            _metadataCreator.Object, _qosCreator.Object,\n            _rloCreator.Object,\n            _lbKeyCreator.Object,\n            _versionCreator.Object,\n            _versionPolicyCreator.Object);\n    }\n\n    [Fact]\n    public void Should_return_nothing()\n    {\n        // Arrange\n        _fileConfig = new FileConfiguration();\n\n        // Act\n        _result = _creator.Create(_fileConfig);\n\n        // Assert\n        _result.Count.ShouldBe(0);\n        _lbKeyCreator.Verify(x => x.Create(It.IsAny<FileDynamicRoute>(), It.IsAny<LoadBalancerOptions>()), Times.Never);\n        _lboCreator.Verify(x => x.Create(It.IsAny<FileDynamicRoute>(), It.IsAny<FileGlobalConfiguration>()), Times.Never);\n        _rloCreator.Verify(x => x.Create(It.IsAny<IRouteRateLimiting>(), It.IsAny<FileGlobalConfiguration>()), Times.Never);\n        _metadataCreator.Verify(x => x.Create(It.IsAny<IDictionary<string, string>>(), It.IsAny<FileGlobalConfiguration>()), Times.Never);\n        _cacheCreator.Verify(x => x.Create(It.IsAny<FileDynamicRoute>(), It.IsAny<FileGlobalConfiguration>(), It.IsAny<string>()), Times.Never);\n        _authCreator.Verify(x => x.Create(It.IsAny<FileDynamicRoute>(), It.IsAny<FileGlobalConfiguration>()), Times.Never);\n    }\n\n    [Fact]\n    public void Should_return_routes()\n    {\n        // Arrange\n        _fileConfig = new FileConfiguration\n        {\n            DynamicRoutes = new()\n            {\n                GivenDynamicRoute(\"1\", false, \"1.1\", \"foo\", \"bar\"),\n                GivenDynamicRoute(\"2\", true, \"2.0\", \"foo\", \"baz\"),\n            },\n        };\n        GivenTheRloCreatorReturns();\n        GivenTheVersionCreatorReturns();\n        GivenTheVersionPolicyCreatorReturns();\n        GivenTheMetadataCreatorReturns();\n\n        // Act\n        _result = _creator.Create(_fileConfig);\n\n        // Assert\n        ThenTheRoutesAreReturned();\n        ThenTheBasicCreatorsAreCalledCorrectly();\n    }\n\n    #region PR 2073\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")] // https://github.com/ThreeMammals/Ocelot/pull/2073\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    [Trait(\"Feat\", \"1869\")] // https://github.com/ThreeMammals/Ocelot/issues/1869\n    public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute()\n    {\n        // Arrange\n        var route = new FileDynamicRoute { Timeout = 11 };\n        var global = new FileGlobalConfiguration { Timeout = 22 };\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Equal(route.Timeout, timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig()\n    {\n        // Arrange\n        var route = new FileDynamicRoute();\n        var global = new FileGlobalConfiguration { Timeout = 22 };\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Null(route.Timeout);\n        Assert.Equal(global.Timeout, timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults()\n    {\n        // Arrange\n        var route = new FileDynamicRoute();\n        var global = new FileGlobalConfiguration();\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Null(route.Timeout);\n        Assert.Null(global.Timeout);\n        Assert.Equal(DownstreamRoute.DefTimeout, timeout);\n    }\n    #endregion\n\n    private static FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, string downstreamHttpVersion, string key, string value) => new()\n    {\n        ServiceName = serviceName,\n        RateLimitRule = new FileRateLimitByHeaderRule\n        {\n            EnableRateLimiting = enableRateLimiting,\n        },\n        DownstreamHttpVersion = downstreamHttpVersion,\n        Metadata = new Dictionary<string, string>\n        {\n            [key] = value,\n        },\n    };\n\n    private void ThenTheBasicCreatorsAreCalledCorrectly()\n    {\n        _fileConfig.DynamicRoutes.ForEach(dynamicRoute =>\n        {\n            _authCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once);\n            _lbKeyCreator.Verify(x => x.Create(dynamicRoute, It.IsAny<LoadBalancerOptions>()), Times.Once);\n            _lboCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once);\n            _rloCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once);\n            _metadataCreator.Verify(x => x.Create(dynamicRoute.Metadata, _fileConfig.GlobalConfiguration), Times.Once);\n            _versionCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersion), Times.Once);\n            _versionPolicyCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersionPolicy), Times.Exactly(2));\n        });\n    }\n\n    private void ThenTheRoutesAreReturned()\n    {\n        _result.Count.ShouldBe(2);\n        for (int i = 0; i < _result.Count; i++)\n        {\n            DownstreamRoute dr = _result[i].DownstreamRoute[0];\n            dr.RateLimitOptions.EnableRateLimiting.ShouldBe(_rlo[i].EnableRateLimiting);\n            dr.RateLimitOptions.ShouldBe(_rlo[i]);\n            dr.DownstreamHttpVersion.ShouldBe(_version);\n            dr.DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy);\n            dr.ServiceName.ShouldBe(_fileConfig.DynamicRoutes[i].ServiceName);\n        }\n    }\n\n    private void GivenTheVersionCreatorReturns()\n    {\n        _version = new Version(\"1.1\");\n        _versionCreator.Setup(x => x.Create(It.IsAny<string>())).Returns(_version);\n    }\n\n    private void GivenTheVersionPolicyCreatorReturns()\n    {\n        _versionPolicy = HttpVersionPolicy.RequestVersionOrLower;\n        _versionPolicyCreator.Setup(x => x.Create(It.IsAny<string>())).Returns(_versionPolicy);\n    }\n\n    private void GivenTheMetadataCreatorReturns()\n    {\n        _expectedMetadata = new()\n        {\n            [\"foo\"] = \"bar\",\n        };\n        _metadataCreator.Setup(x => x.Create(It.IsAny<IDictionary<string, string>>(), It.IsAny<FileGlobalConfiguration>()))\n            .Returns(new MetadataOptions() { Metadata = _expectedMetadata });\n    }\n\n    private void GivenTheRloCreatorReturns()\n    {\n        _rlo = [\n            new() { EnableRateLimiting = false },\n            new() { EnableRateLimiting = true },\n        ];\n        _rloCreator\n            .SetupSequence(x => x.Create(It.IsAny<IRouteRateLimiting>(), It.IsAny<FileGlobalConfiguration>()))\n            .Returns(_rlo[0])\n            .Returns(_rlo[1]);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs",
    "content": "using Ocelot.Configuration;\r\nusing Ocelot.Configuration.Builder;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Configuration.Repository;\r\nusing Ocelot.Configuration.Setter;\r\nusing Ocelot.Errors;\r\nusing Ocelot.Responses;\r\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class FileConfigurationSetterTests : UnitTest\r\n{\r\n    private FileConfiguration _fileConfiguration;\r\n    private readonly FileAndInternalConfigurationSetter _configSetter;\r\n    private readonly Mock<IInternalConfigurationRepository> _configRepo;\r\n    private readonly Mock<IInternalConfigurationCreator> _configCreator;\r\n    private Response<IInternalConfiguration> _configuration;\r\n    private object _result;\r\n    private readonly Mock<IFileConfigurationRepository> _repo;\r\n\r\n    public FileConfigurationSetterTests()\r\n    {\r\n        _repo = new Mock<IFileConfigurationRepository>();\r\n        _configRepo = new Mock<IInternalConfigurationRepository>();\r\n        _configCreator = new Mock<IInternalConfigurationCreator>();\r\n        _configSetter = new FileAndInternalConfigurationSetter(_configRepo.Object, _configCreator.Object, _repo.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_set_configuration()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = new FileConfiguration();\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        var config = new InternalConfiguration()\r\n        {\r\n            AdministrationPath = string.Empty,\r\n            ServiceProviderConfiguration = serviceProviderConfig,\r\n            RequestId = \"asdf\",\r\n            LoadBalancerOptions = new(),\r\n            DownstreamScheme = string.Empty,\r\n            QoSOptions = new(),\r\n            HttpHandlerOptions = new(),\r\n            DownstreamHttpVersion = new Version(\"1.1\"),\r\n            DownstreamHttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,\r\n            MetadataOptions = new(),\r\n            RateLimitOptions = new(),\r\n            Timeout = 111,\r\n        };\r\n        GivenTheRepoReturns(new OkResponse());\r\n        GivenTheCreatorReturns(new OkResponse<IInternalConfiguration>(config));\r\n\r\n        // Act\r\n        _result = await _configSetter.Set(_fileConfiguration);\r\n\r\n        // Assert\r\n        ThenTheConfigurationRepositoryIsCalledCorrectly();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_error_if_unable_to_set_file_configuration()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = new FileConfiguration();\r\n        GivenTheRepoReturns(new ErrorResponse(It.IsAny<Error>()));\r\n\r\n        // Act\r\n        _result = await _configSetter.Set(_fileConfiguration);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<ErrorResponse>();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_error_if_unable_to_set_ocelot_configuration()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = new FileConfiguration();\r\n        GivenTheRepoReturns(new OkResponse());\r\n        GivenTheCreatorReturns(new ErrorResponse<IInternalConfiguration>(It.IsAny<Error>()));\r\n\r\n        // Act\r\n        _result = await _configSetter.Set(_fileConfiguration);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<ErrorResponse>();\r\n    }\r\n\r\n    private void GivenTheRepoReturns(Response response)\r\n    {\r\n        _repo\r\n            .Setup(x => x.Set(It.IsAny<FileConfiguration>()))\r\n            .ReturnsAsync(response);\r\n    }\r\n\r\n    private void GivenTheCreatorReturns(Response<IInternalConfiguration> configuration)\r\n    {\r\n        _configuration = configuration;\r\n        _configCreator\r\n            .Setup(x => x.Create(_fileConfiguration))\r\n            .ReturnsAsync(_configuration);\r\n    }\r\n\r\n    private void ThenTheConfigurationRepositoryIsCalledCorrectly()\r\n    {\r\n        _configRepo.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\nusing Ocelot.Errors;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class FileInternalConfigurationCreatorTests : UnitTest\r\n{\r\n    private readonly Mock<IConfigurationValidator> _validator;\r\n    private readonly Mock<IRoutesCreator> _routesCreator;\r\n    private readonly Mock<IAggregatesCreator> _aggregatesCreator;\r\n    private readonly Mock<IDynamicsCreator> _dynamicsCreator;\r\n    private readonly Mock<IConfigurationCreator> _configCreator;\r\n    private FileConfiguration _fileConfiguration;\r\n    private readonly FileInternalConfigurationCreator _creator;\r\n    private Response<IInternalConfiguration> _result;\r\n    private List<Route> _routes;\r\n    private List<Route> _aggregates;\r\n    private List<Route> _dynamics;\r\n    private InternalConfiguration _internalConfig;\r\n\r\n    public FileInternalConfigurationCreatorTests()\r\n    {\r\n        _validator = new Mock<IConfigurationValidator>();\r\n        _routesCreator = new Mock<IRoutesCreator>();\r\n        _aggregatesCreator = new Mock<IAggregatesCreator>();\r\n        _dynamicsCreator = new Mock<IDynamicsCreator>();\r\n        _configCreator = new Mock<IConfigurationCreator>();\r\n\r\n        _creator = new FileInternalConfigurationCreator(_validator.Object, _routesCreator.Object, _aggregatesCreator.Object, _dynamicsCreator.Object, _configCreator.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_validation_error()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = new FileConfiguration();\r\n        GivenTheValidationFails();\r\n\r\n        // Act\r\n        _result = await _creator.Create(_fileConfiguration);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_internal_configuration()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = new FileConfiguration();\r\n        GivenTheValidationSucceeds();\r\n        GivenTheDependenciesAreSetUp();\r\n\r\n        // Act\r\n        _result = await _creator.Create(_fileConfiguration);\r\n\r\n        // Assert\r\n        ThenTheDependenciesAreCalledCorrectly();\r\n    }\r\n\r\n    private void ThenTheDependenciesAreCalledCorrectly()\r\n    {\r\n        _routesCreator.Verify(x => x.Create(_fileConfiguration), Times.Once);\r\n        _aggregatesCreator.Verify(x => x.Create(_fileConfiguration, _routes), Times.Once);\r\n        _dynamicsCreator.Verify(x => x.Create(_fileConfiguration), Times.Once);\r\n\r\n        var mergedRoutes = _routes\r\n            .Union(_aggregates)\r\n            .Union(_dynamics)\r\n            .ToArray();\r\n\r\n        _configCreator.Verify(x => x.Create(_fileConfiguration, It.Is<Route[]>(y => y.Length == mergedRoutes.Length)), Times.Once);\r\n    }\r\n\r\n    private void GivenTheDependenciesAreSetUp()\r\n    {\r\n        _routes = new List<Route> { new() };\r\n        _aggregates = new List<Route> { new() };\r\n        _dynamics = new List<Route> { new() };\r\n        _internalConfig = new InternalConfiguration();\r\n\r\n        _routesCreator.Setup(x => x.Create(It.IsAny<FileConfiguration>())).Returns(_routes);\r\n        _aggregatesCreator.Setup(x => x.Create(It.IsAny<FileConfiguration>(), It.IsAny<List<Route>>())).Returns(_aggregates);\r\n        _dynamicsCreator.Setup(x => x.Create(It.IsAny<FileConfiguration>())).Returns(_dynamics);\r\n        _configCreator.Setup(x => x.Create(It.IsAny<FileConfiguration>(), It.IsAny<Route[]>())).Returns(_internalConfig);\r\n    }\r\n\r\n    private void GivenTheValidationSucceeds()\r\n    {\r\n        var ok = new ConfigurationValidationResult(false);\r\n        var response = new OkResponse<ConfigurationValidationResult>(ok);\r\n        _validator.Setup(x => x.IsValid(It.IsAny<FileConfiguration>())).ReturnsAsync(response);\r\n    }\r\n\r\n    private void GivenTheValidationFails()\r\n    {\r\n        var error = new ConfigurationValidationResult(true, new List<Error> { new AnyError() });\r\n        var response = new OkResponse<ConfigurationValidationResult>(error);\r\n        _validator.Setup(x => x.IsValid(It.IsAny<FileConfiguration>())).ReturnsAsync(response);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration.FileModels;\n\npublic class FileDynamicRouteTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        FileDynamicRoute instance = new();\n\n        // Assert\n        Assert.Null(instance.Metadata);\n        Assert.Null(instance.Key);\n        Assert.Null(instance.RateLimitRule);\n        Assert.Null(instance.RateLimitOptions);\n    }\n\n    [Fact]\n    public void Ctor_IRouteGrouping_IsImplemented()\n    {\n        // Arrange, Act\n        FileDynamicRoute instance = new() { Key = \"abc\" };\n\n        // Assert\n        Assert.IsAssignableFrom<IRouteGrouping>(instance);\n        IRouteGrouping obj = instance;\n        Assert.Equal(\"abc\", obj.Key);\n    }\n\n    [Fact]\n    public void Ctor_IRouteRateLimiting_IsImplemented()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule rule = new() { ClientIdHeader = \"111\" };\n\n        // Act\n        FileDynamicRoute instance = new() { RateLimitOptions = rule };\n\n        // Assert\n        Assert.IsAssignableFrom<IRouteRateLimiting>(instance);\n        IRouteRateLimiting obj = instance;\n        Assert.Equal(rule, obj.RateLimitOptions);\n        Assert.Equal(\"111\", obj.RateLimitOptions.ClientIdHeader);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileModels/FileGlobalHttpHandlerOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration.FileModels;\n\n[Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n[Trait(\"Feat\", \"2320\")] // https://github.com/ThreeMammals/Ocelot/issues/2320\npublic class FileGlobalHttpHandlerOptionsTests\n{\n    [Fact]\n    public void Ctor_Default()\n    {\n        // Arrange, Act\n        FileGlobalHttpHandlerOptions actual = new();\n\n        // Assert\n        Assert.Null(actual.RouteKeys);\n        Assert.Null(actual.UseTracing);\n    }\n\n    [Fact]\n    public void Ctor_FileHttpHandlerOptions()\n    {\n        // Arrange\n        FileHttpHandlerOptions from = new()\n        {\n            AllowAutoRedirect = true,\n            MaxConnectionsPerServer = 111,\n            PooledConnectionLifetimeSeconds = 222,\n            UseCookieContainer = true,\n            UseProxy = true,\n            UseTracing = true,\n        };\n\n        // Act\n        FileGlobalHttpHandlerOptions actual = new(from);\n\n        // Assert\n        Assert.Null(actual.RouteKeys);\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n        Assert.True(actual.AllowAutoRedirect);\n        Assert.Equal(111, actual.MaxConnectionsPerServer);\n        Assert.Equal(222, actual.PooledConnectionLifetimeSeconds);\n        Assert.True(actual.UseCookieContainer);\n        Assert.True(actual.UseProxy);\n        Assert.True(actual.UseTracing);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileModels/FileMetadataOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing System.Globalization;\n\nnamespace Ocelot.UnitTests.Configuration.FileModels;\n\npublic class FileMetadataOptionsTests\n{\n    [Fact]\n    public void Ctor_Default()\n    {\n        // Arrange, Act\n        FileMetadataOptions actual = new();\n\n        // Assert\n        Assert.Contains(\",\", actual.Separators);\n        Assert.Contains(\" \", actual.TrimChars);\n    }\n\n    [Fact]\n    public void Ctor_CopyingFrom()\n    {\n        // Arrange\n        FileMetadataOptions from = new()\n        {\n            CurrentCulture = CultureInfo.GetCultureInfo(\"uk\").Name,\n            NumberStyle = NumberStyles.None.ToString(),\n            Separators = [\"|\"],\n            StringSplitOption = StringSplitOptions.TrimEntries.ToString(),\n            TrimChars = ['x'],\n        };\n\n        // Act\n        FileMetadataOptions actual = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n        Assert.Equal(\"uk\", actual.CurrentCulture);\n        Assert.Equal(\"None\", actual.NumberStyle);\n        Assert.Contains(\"|\", actual.Separators);\n        Assert.Equal(\"TrimEntries\", actual.StringSplitOption);\n        Assert.Contains('x', actual.TrimChars);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration.FileModels;\n\npublic class FileRouteTests : UnitTest\n{\n    [Fact]\n    [Trait(\"PR\", \"1753\")]\n    public void Ctor_Copying_Copied()\n    {\n        // Arrange\n        var expected = GivenFileRoute();\n\n        // Act\n        FileRoute actual = new(expected); // copying\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1753\")]\n    public void Clone_ShouldClone()\n    {\n        // Arrange\n        var expected = GivenFileRoute();\n\n        // Act\n        var obj = expected.Clone();\n        var actual = Assert.IsType<FileRoute>(obj);\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void ToString_NoKeyNoDiscovery_ShouldBeUpstreamPathTemplate()\n    {\n        // Arrange\n        var route = GivenFileRoute();\n        route.Key = null;\n        route.ServiceName = null;\n        route.UpstreamPathTemplate = \"/upstream\";\n\n        // Act\n        var actual = route.ToString();\n\n        // Assert\n        Assert.Equal(\"/upstream\", actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, \"?\")]\n    [InlineData(\"/downstream\", \"/downstream\")]\n    public void ToString_NoKeyNoUpstreamPathTemplate_ShouldBeDownstreamPathTemplate(string downstream, string expected)\n    {\n        // Arrange\n        var route = GivenFileRoute();\n        route.Key = null;\n        route.ServiceName = null;\n        route.UpstreamPathTemplate = null;\n        route.DownstreamPathTemplate = downstream;\n\n        // Act\n        var actual = route.ToString();\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void ToString_NoKeyWithServiceDiscovery_ShouldContainServiceName()\n    {\n        // Arrange\n        var route = GivenFileRoute();\n        route.Key = null;\n        route.ServiceName = TestName();\n        route.UpstreamPathTemplate = \"/upstream\";\n\n        // Act\n        var actual = route.ToString();\n\n        // Assert\n        Assert.True(actual.Contains(route.ServiceName));\n        Assert.Equal(\"test-namespace:ToString_NoKeyWithServiceDiscovery_ShouldContainServiceName:/upstream\", actual);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void ToString_HasKey_ShouldBeKey()\n    {\n        // Arrange\n        var route = GivenFileRoute();\n        route.Key = TestName();\n\n        // Act\n        var actual = route.ToString();\n\n        // Assert\n        Assert.Equal(nameof(ToString_HasKey_ShouldBeKey), actual);\n    }\n\n    private static FileRoute GivenFileRoute()\n    {\n        FileRoute expected = new();\n        expected.AddClaimsToRequest.Add(\"key1\", \"value1\");\n        expected.AddHeadersToRequest.Add(\"key2\", \"value2\");\n        expected.AddQueriesToRequest.Add(\"key3\", \"value3\");\n        expected.AuthenticationOptions = new(\"value4\");\n        expected.ChangeDownstreamPathTemplate.Add(\"key5\", \"value5\");\n        expected.DangerousAcceptAnyServerCertificateValidator = true;\n        expected.DelegatingHandlers.Add(\"value6\");\n        expected.DownstreamHeaderTransform.Add(\"key7\", \"value7\");\n        expected.DownstreamHostAndPorts.Add(new(\"host8\", 8));\n        expected.DownstreamHttpMethod = \"value9\";\n        expected.DownstreamHttpVersion = \"value10\";\n        expected.DownstreamHttpVersionPolicy = \"value11\";\n        expected.DownstreamPathTemplate = \"value12\";\n        expected.DownstreamScheme = \"value13\";\n        expected.CacheOptions = new() { Header = \"value14\" };\n        expected.FileCacheOptions = new() { TtlSeconds = 14 };\n        expected.HttpHandlerOptions = new() { MaxConnectionsPerServer = 15 };\n        expected.Key = \"value16\";\n        expected.LoadBalancerOptions ??= new(\"value17\");\n        expected.Metadata ??= new Dictionary<string, string>() { { \"key18\", \"value18\" } };\n        expected.Priority = 19;\n        expected.QoSOptions = new() { DurationOfBreak = 20 };\n        expected.RateLimitOptions ??= new() { Period = \"value21\" };\n        expected.RequestIdKey = \"value22\";\n        expected.RouteClaimsRequirement.Add(\"key23\", \"value23\");\n        expected.RouteIsCaseSensitive = true;\n        expected.SecurityOptions.IPAllowedList.Add(\"value24\");\n        expected.ServiceName = \"test-service\";\n        expected.ServiceNamespace = \"test-namespace\";\n        expected.Timeout = 27;\n        expected.UpstreamHeaderTemplates.Add(\"key28\", \"value28\");\n        expected.UpstreamHeaderTransform.Add(\"key29\", \"value29\");\n        expected.UpstreamHost = \"value30\";\n        expected.UpstreamHttpMethod.Add(\"value31\");\n        expected.UpstreamPathTemplate = \"value32\";\n        return expected;\n    }\n\n    private static void AssertEquality(FileRoute actual, FileRoute expected)\n    {\n        Assert.Equal(expected.AddClaimsToRequest, actual.AddClaimsToRequest);\n        Assert.Equal(expected.AddHeadersToRequest, actual.AddHeadersToRequest);\n        Assert.Equal(expected.AddQueriesToRequest, actual.AddQueriesToRequest);\n        Assert.Equivalent(expected.AuthenticationOptions, actual.AuthenticationOptions); // FileAuthenticationOptions requires Equals overriding\n        Assert.Equal(expected.ChangeDownstreamPathTemplate, actual.ChangeDownstreamPathTemplate);\n        Assert.Equal(expected.DangerousAcceptAnyServerCertificateValidator, actual.DangerousAcceptAnyServerCertificateValidator);\n        Assert.Equal(expected.DelegatingHandlers, actual.DelegatingHandlers);\n        Assert.Equal(expected.DownstreamHeaderTransform, actual.DownstreamHeaderTransform);\n        Assert.Equivalent(expected.DownstreamHostAndPorts, actual.DownstreamHostAndPorts); // FileHostAndPort requires Equals overriding\n        Assert.Equal(expected.DownstreamHttpMethod, actual.DownstreamHttpMethod);\n        Assert.Equal(expected.DownstreamHttpVersion, actual.DownstreamHttpVersion);\n        Assert.Equal(expected.DownstreamHttpVersionPolicy, actual.DownstreamHttpVersionPolicy);\n        Assert.Equal(expected.DownstreamPathTemplate, actual.DownstreamPathTemplate);\n        Assert.Equal(expected.DownstreamScheme, actual.DownstreamScheme);\n        Assert.Equivalent(expected.CacheOptions, actual.CacheOptions); // FileCacheOptions requires Equals overriding\n        Assert.Equivalent(expected.FileCacheOptions, actual.FileCacheOptions); // FileCacheOptions requires Equals overriding\n        Assert.Equivalent(expected.HttpHandlerOptions, actual.HttpHandlerOptions); // FileHttpHandlerOptions requires Equals overriding\n        Assert.Equal(expected.Key, actual.Key);\n        Assert.Equivalent(expected.LoadBalancerOptions, actual.LoadBalancerOptions); // FileLoadBalancerOptions requires Equals overriding\n        Assert.Equal(expected.Metadata, actual.Metadata);\n        Assert.Equal(expected.Priority, actual.Priority);\n        Assert.Equivalent(expected.QoSOptions, actual.QoSOptions); // FileQoSOptions requires Equals overriding\n        Assert.Equivalent(expected.RateLimitOptions, actual.RateLimitOptions); // FileRateLimitByHeaderRule requires Equals overriding\n        Assert.Equal(expected.RequestIdKey, actual.RequestIdKey);\n        Assert.Equal(expected.RouteClaimsRequirement, actual.RouteClaimsRequirement);\n        Assert.Equal(expected.RouteIsCaseSensitive, actual.RouteIsCaseSensitive);\n        Assert.Equivalent(expected.SecurityOptions, actual.SecurityOptions); // FileSecurityOptions requires Equals overriding\n        Assert.Equal(expected.ServiceName, actual.ServiceName);\n        Assert.Equal(expected.ServiceNamespace, actual.ServiceNamespace);\n        Assert.Equal(expected.Timeout, actual.Timeout);\n        Assert.Equal(expected.UpstreamHeaderTemplates, actual.UpstreamHeaderTemplates);\n        Assert.Equal(expected.UpstreamHeaderTransform, actual.UpstreamHeaderTransform);\n        Assert.Equal(expected.UpstreamHost, actual.UpstreamHost);\n        Assert.Equal(expected.UpstreamHttpMethod, actual.UpstreamHttpMethod);\n        Assert.Equal(expected.UpstreamPathTemplate, actual.UpstreamPathTemplate);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HashCreationTests.cs",
    "content": "using Microsoft.AspNetCore.Cryptography.KeyDerivation;\nusing System.Security.Cryptography;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class HashCreationTests\r\n{\r\n    [Fact]\r\n    public void Should_create_hash_and_salt()\r\n    {\r\n        var password = \"secret\";\r\n        var salt = new byte[128 / 8];\r\n\r\n        using var rng = RandomNumberGenerator.Create();\r\n        rng.GetBytes(salt);\r\n\r\n        var storedSalt = Convert.ToBase64String(salt);\n        var storedHash = Convert.ToBase64String(KeyDerivation.Pbkdf2(\r\n            password: password,\r\n            salt: salt,\r\n            prf: KeyDerivationPrf.HMACSHA256,\r\n            iterationCount: 10000,\r\n            numBytesRequested: 256 / 8));\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs",
    "content": "using Microsoft.Extensions.Options;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Infrastructure;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Responses;\r\nusing Ocelot.UnitTests.Responder;\r\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class HeaderFindAndReplaceCreatorTests : UnitTest\r\n{\r\n    private readonly HeaderFindAndReplaceCreator _creator;\r\n    private readonly FileGlobalConfiguration _global;\r\n    private HeaderTransformations _result;\r\n    private readonly Mock<IPlaceholders> _placeholders;\r\n    private readonly Mock<IOcelotLoggerFactory> _factory;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n    private readonly List<Func<string>> _messages = new();\r\n\r\n    public HeaderFindAndReplaceCreatorTests()\r\n    {\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\r\n            .Callback<Func<string>>(_messages.Add);\r\n\r\n        _factory = new Mock<IOcelotLoggerFactory>();\r\n        _factory.Setup(x => x.CreateLogger<HeaderFindAndReplaceCreator>()).Returns(_logger.Object);\r\n        _placeholders = new Mock<IPlaceholders>();\r\n\r\n        _global = new FileGlobalConfiguration();\r\n        _global.UpstreamHeaderTransform.Add(\"TestGlobal\", \"Test, Chicken\");\r\n        _global.UpstreamHeaderTransform.Add(\"MoopGlobal\", \"o, a\");\r\n        _global.DownstreamHeaderTransform.Add(\"PopGlobal\", \"West, East\");\r\n        _global.DownstreamHeaderTransform.Add(\"BopGlobal\", \"e, r\");\r\n\r\n        var options = new Mock<IOptions<FileGlobalConfiguration>>();\r\n        options.Setup(x => x.Value).Returns(_global);\r\n        _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object, options.Object);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"204\")] // https://github.com/ThreeMammals/Ocelot/pull/204\r\n    [Trait(\"Feat\", \"1658\")] // https://github.com/ThreeMammals/Ocelot/issues/1658\r\n    [Trait(\"PR\", \"1659\")] // https://github.com/ThreeMammals/Ocelot/pull/1659\r\n    public void Should_create()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Test\", \"Test, Chicken\"},\r\n                {\"Moop\", \"o, a\"},\r\n            },\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Pop\", \"West, East\"},\r\n                {\"Bop\", \"e, r\"},\r\n            },\r\n        };\r\n        var upstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"Test\", \"Test\", \"Chicken\", 0),\r\n            new(\"Moop\", \"o\", \"a\", 0),\r\n            new(\"TestGlobal\", \"Test\", \"Chicken\", 0),\r\n            new(\"MoopGlobal\", \"o\", \"a\", 0),\r\n        };\r\n        var downstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"Pop\", \"West\", \"East\", 0),\r\n            new(\"Bop\", \"e\", \"r\", 0),\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingUpstreamIsReturned(upstream);\r\n        ThenTheFollowingDownstreamIsReturned(downstream);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"1658\")]\r\n    public void Create_WithRouteAndWithoutGlobalConfigurationParam_GlobalConfigurationInjectionIsReused()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute(); // no data\r\n        var upstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(_global.UpstreamHeaderTransform.First()),\r\n            new(_global.UpstreamHeaderTransform.Last()),\r\n        };\r\n        var downstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n\r\n        // Act\r\n        _result = _creator.Create(route, null);\r\n\r\n        // Assert\r\n        ThenTheFollowingUpstreamIsReturned(upstream);\r\n        ThenTheFollowingDownstreamIsReturned(downstream);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"623\")] // https://github.com/ThreeMammals/Ocelot/issues/623\r\n    [Trait(\"PR\", \"632\")] // https://github.com/ThreeMammals/Ocelot/pull/632\r\n    public void Should_create_with_add_headers_to_request()\r\n    {\r\n        // Arrange\r\n        const string key = \"X-Forwarded-For\";\r\n        const string value = \"{RemoteIpAddress}\";\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {key, value},\r\n            },\r\n        };\r\n        var expected = new AddHeader(key, value);\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingAddHeaderToUpstreamIsReturned(expected);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_base_url_placeholder()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Location\", \"http://www.bbc.co.uk/, {BaseUrl}\"},\r\n            },\r\n        };\r\n        var downstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"Location\", \"http://www.bbc.co.uk/\", \"http://ocelot.com/\", 0),\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n        GivenThePlaceholderIs(\"http://ocelot.com/\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingDownstreamIsReturned(downstream);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"204\")]\r\n    [Trait(\"Feat\", \"1658\")]\r\n    public void Should_log_errors_and_not_add_headers()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Location\", \"http://www.bbc.co.uk/, {BaseUrl}\"},\r\n            },\r\n            UpstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Location\", \"http://www.bbc.co.uk/, {BaseUrl}\"},\r\n            },\r\n        };\r\n        var expectedDownstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n        var expectedUpstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"TestGlobal\", \"Test\", \"Chicken\", 0),\r\n            new(\"MoopGlobal\", \"o\", \"a\", 0),\r\n        };\r\n        GivenTheBaseUrlErrors();\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingDownstreamIsReturned(expectedDownstream);\r\n        ThenTheFollowingUpstreamIsReturned(expectedUpstream);\r\n        ThenTheLoggerIsCalledCorrectly(4,\r\n            \"HeaderFindAndReplace was not mapped from [Location, http://www.bbc.co.uk/, {BaseUrl}] due to UnknownError: blahh\",\r\n            \"Unable to add UpstreamHeaderTransform [Location, http://www.bbc.co.uk/, {BaseUrl}]\",\r\n            \"HeaderFindAndReplace was not mapped from [Location, http://www.bbc.co.uk/, {BaseUrl}] due to UnknownError: blahh\",\r\n            \"Unable to add DownstreamHeaderTransform [Location, http://www.bbc.co.uk/, {BaseUrl}]\");\r\n    }\r\n\r\n    private void ThenTheLoggerIsCalledCorrectly(int times, params string[] messages)\r\n    {\r\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Exactly(times));\r\n        _messages.ShouldNotBeEmpty();\r\n        var actual = _messages.Select(f => f.Invoke()).ToList();\r\n        foreach (var expected in messages)\r\n        {\r\n            actual.ShouldContain(expected);\r\n        }\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_base_url_partial_placeholder()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Location\", \"http://www.bbc.co.uk/pay, {BaseUrl}pay\"},\r\n            },\r\n        };\r\n        var downstream = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"Location\", \"http://www.bbc.co.uk/pay\", \"http://ocelot.com/pay\", 0),\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n        GivenThePlaceholderIs(\"http://ocelot.com/\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingDownstreamIsReturned(downstream);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"204\")]\r\n    public void Should_map_with_partial_placeholder_in_the_middle()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Host-Next\", \"www.bbc.co.uk, subdomain.{Host}/path\"},\r\n            },\r\n        };\r\n        var expected = new List<HeaderFindAndReplace>\r\n        {\r\n            new(\"Host-Next\", \"www.bbc.co.uk\", \"subdomain.ocelot.next/path\", 0),\r\n            new(_global.DownstreamHeaderTransform.First()),\r\n            new(_global.DownstreamHeaderTransform.Last()),\r\n        };\r\n        GivenThePlaceholderIs(\"ocelot.next\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingDownstreamIsReturned(expected);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_trace_id_header()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"Trace-Id\", \"{TraceId}\"},\r\n            },\r\n        };\r\n        var expected = new AddHeader(\"Trace-Id\", \"{TraceId}\");\r\n        GivenThePlaceholderIs(\"http://ocelot.com/\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingAddHeaderToDownstreamIsReturned(expected);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_downstream_header_as_is_when_no_replacement_is_given()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"X-Custom-Header\", \"Value\"},\r\n            },\r\n        };\r\n        var expected = new AddHeader(\"X-Custom-Header\", \"Value\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingAddHeaderToDownstreamIsReturned(expected);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_upstream_header_as_is_when_no_replacement_is_given()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamHeaderTransform = new Dictionary<string, string>\r\n            {\r\n                {\"X-Custom-Header\", \"Value\"},\r\n            },\r\n        };\r\n        var expected = new AddHeader(\"X-Custom-Header\", \"Value\");\r\n\r\n        // Act\r\n        _result = _creator.Create(route);\r\n\r\n        // Assert\r\n        ThenTheFollowingAddHeaderToUpstreamIsReturned(expected);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"1659\")]\r\n    [Trait(\"Feat\", \"1658\")]\r\n    public void Merge_ShouldMergeGlobalIntoRouteOpts()\r\n    {\r\n        // Arrange\r\n        var routeTransforms = new Dictionary<string, string>()\r\n        {\r\n            { \"B\", \"routeB\" },\r\n            { \"C\", \"routeC\" },\r\n        };\r\n        var globalTransforms = new Dictionary<string, string>()\r\n        {\r\n            { \"A\", \"globalA\" },\r\n            { \"B\", \"globalB\" },\r\n        };\r\n\r\n        // Act\r\n        var actual = HeaderFindAndReplaceCreator.Merge(routeTransforms, globalTransforms);\r\n\r\n        // Assert\r\n        actual.ShouldNotBeNull();\r\n        var dictionary = actual.ToDictionary(x => x.Key, x => x.Value);\r\n        dictionary.Count.ShouldBe(3);\r\n        dictionary.ContainsKey(\"A\").ShouldBeTrue();\r\n        dictionary[\"A\"].ShouldBe(\"globalA\");\r\n        dictionary.ContainsKey(\"B\").ShouldBeTrue();\r\n        dictionary[\"B\"].ShouldBe(\"routeB\"); // local value wins over global one\r\n        dictionary.ContainsKey(\"C\").ShouldBeTrue();\r\n        dictionary[\"C\"].ShouldBe(\"routeC\");\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"1659\")]\r\n    [Trait(\"Feat\", \"1658\")]\r\n    public void Merge_NullParams_NullChecksHaveBeenPerformed()\r\n    {\r\n        // Arrange, Act\r\n        var actual = HeaderFindAndReplaceCreator.Merge(null, null);\r\n\r\n        // Assert\r\n        actual.ShouldNotBeNull().ShouldBeEmpty();\r\n    }\r\n\r\n    private void GivenThePlaceholderIs(string placeholderValue)\r\n    {\r\n        _placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new OkResponse<string>(placeholderValue));\r\n    }\r\n\r\n    private void GivenTheBaseUrlErrors()\r\n    {\r\n        _placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new ErrorResponse<string>(new AnyError()));\r\n    }\r\n\r\n    private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader)\r\n    {\r\n        _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key);\r\n        _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value);\r\n    }\r\n\r\n    private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader)\r\n    {\r\n        _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key);\r\n        _result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value);\r\n    }\r\n\r\n    private void ThenTheFollowingDownstreamIsReturned(List<HeaderFindAndReplace> downstream)\r\n    {\r\n        _result.Downstream.Count.ShouldBe(downstream.Count);\r\n\r\n        for (var i = 0; i < _result.Downstream.Count; i++)\r\n        {\r\n            var result = _result.Downstream[i];\r\n            var expected = downstream[i];\r\n            result.Find.ShouldBe(expected.Find);\r\n            result.Index.ShouldBe(expected.Index);\r\n            result.Key.ShouldBe(expected.Key);\r\n            result.Replace.ShouldBe(expected.Replace);\r\n        }\r\n    }\r\n\r\n    private void ThenTheFollowingUpstreamIsReturned(List<HeaderFindAndReplace> expecteds)\r\n    {\r\n        _result.Upstream.Count.ShouldBe(expecteds.Count);\r\n\r\n        for (var i = 0; i < _result.Upstream.Count; i++)\r\n        {\r\n            var result = _result.Upstream[i];\r\n            var expected = expecteds[i];\r\n            result.Find.ShouldBe(expected.Find);\r\n            result.Index.ShouldBe(expected.Index);\r\n            result.Key.ShouldBe(expected.Key);\r\n            result.Replace.ShouldBe(expected.Replace);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceTests.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class HeaderFindAndReplaceTests\n{\n    [Fact]\n    [Trait(\"PR\", \"1659\")]\n    public void Ctor_Copying_Copied()\n    {\n        // Arrange\n        HeaderFindAndReplace expected = new(\"1\", \"2\", \"3\", 4);\n\n        // Act\n        HeaderFindAndReplace actual = new(expected); // copying\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1659\")]\n    public void Ctor_KeyValuePair_Copied()\n    {\n        // Arrange\n        KeyValuePair<string, string> from = new(\"Location\", \"XXX, YYY\");\n        HeaderFindAndReplace expected = new(\"Location\", \"XXX\", \"YYY\", 0);\n\n        // Act\n        HeaderFindAndReplace actual = new(from); // copying\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    private const string Em = \"\";\n\n    [Theory]\n    [Trait(\"PR\", \"1659\")]\n    [InlineData(null, Em, Em)]\n    [InlineData(\"\", Em, Em)]\n    [InlineData(\" \", Em, Em)]\n    [InlineData(\"x\", \"x\", Em)]\n    [InlineData(\"x,y\", \"x\", \"y\")]\n    public void Ctor_KeyValuePair_ArgIsChecked(string value, string find, string replace)\n    {\n        // Arrange\n        KeyValuePair<string, string> from = new(\"key\", value);\n\n        // Act\n        HeaderFindAndReplace actual = new(from);\n\n        // Assert\n        Assert.Equal(0, actual.Index);\n        Assert.Equal(\"key\", actual.Key);\n        Assert.Equal(find, actual.Find);\n        Assert.Equal(replace, actual.Replace);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1659\")]\n    public void ToString_Serialized()\n    {\n        // Arrange\n        HeaderFindAndReplace headerFR = new(\"Location\", \"XXX\", \"YYY\", 3);\n\n        // Act\n        var actual = headerFR.ToString();\n\n        // Assert\n        Assert.Equal(\"HeaderFindAndReplace[Location at 3: XXX -> YYY]\", actual);\n    }\n\n    private static void AssertEquality(HeaderFindAndReplace actual, HeaderFindAndReplace expected)\n    {\n        Assert.Equal(expected.Index, actual.Index);\n        Assert.Equal(expected.Key, actual.Key);\n        Assert.Equal(expected.Find, actual.Find);\n        Assert.Equal(expected.Replace, actual.Replace);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\r\nusing System.Reflection;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class HttpHandlerOptionsCreatorTests : UnitTest\r\n{\r\n    private HttpHandlerOptionsCreator _creator;\r\n    private readonly Mock<IOcelotTracer> _tracer = new();\r\n\r\n    public HttpHandlerOptionsCreatorTests()\r\n    {\r\n        Arrange();\r\n    }\r\n\r\n    private void Arrange(bool hasTracer = true)\r\n    {\r\n        var services = new ServiceCollection();\r\n        if (hasTracer)\n            services.AddSingleton<IOcelotTracer>(_tracer.Object);\n\r\n        var provider = services.BuildServiceProvider(true);\n        _creator = new HttpHandlerOptionsCreator(provider);\r\n    }\r\n\r\n    [Fact]\r\n    public void Ctor()\r\n    {\r\n        // Act\r\n        Arrange();\r\n\r\n        // Assert\r\n        var field = _creator.GetType().GetField(nameof(_tracer), BindingFlags.Instance | BindingFlags.NonPublic);\r\n        Assert.NotNull(field);\r\n        var tracer = field.GetValue(_creator) as IOcelotTracer;\r\n        Assert.NotNull(tracer);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(true, true, false)]\r\n    [InlineData(false, false, false)]\r\n    [InlineData(false, true, true)]\r\n    public void Create_FileHttpHandlerOptions(bool isNull, bool hasTracer, bool expectedUseTracing)\r\n    {\r\n        Arrange(hasTracer);\r\n        FileHttpHandlerOptions opts = isNull ? null : new()\r\n        {\r\n            UseTracing = true,\r\n        };\r\n\r\n        // Act\r\n        var actual = _creator.Create(opts);\r\n\r\n        // Assert\r\n        Assert.Equal(expectedUseTracing, actual.UseTracing);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void Create_FromRoute_NullChecks()\r\n    {\r\n        // Arrange, Act, Assert\r\n        FileRoute route = null;\r\n        FileGlobalConfiguration globalConfiguration = null;\r\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\r\n        Assert.Equal(nameof(route), actual.ParamName);\r\n\r\n        // Arrange, Act, Assert 2\r\n        route = new();\r\n        globalConfiguration = null;\r\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\r\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void Create_FromRoute()\r\n    {\r\n        // Arrange\r\n        var opts = RouteOptions();\r\n        opts.AllowAutoRedirect = null;\r\n        opts.MaxConnectionsPerServer = null;\r\n        var route = GivenRoute(opts);\r\n\r\n        // Act\r\n        var actual = _creator.Create(route, GlobalConfiguration());\r\n\r\n        // Assert\r\n        Assert.False(actual.AllowAutoRedirect);\r\n        Assert.Equal(111, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void Create_FromDynamicRoute_NullChecks()\r\n    {\r\n        // Arrange, Act, Assert\r\n        FileDynamicRoute route = null;\r\n        FileGlobalConfiguration globalConfiguration = null;\r\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\r\n        Assert.Equal(nameof(route), actual.ParamName);\r\n\r\n        // Arrange, Act, Assert 2\r\n        route = new();\r\n        globalConfiguration = null;\r\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\r\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void Create_FromDynamicRoute()\r\n    {\r\n        // Arrange\r\n        var opts = RouteOptions();\r\n        opts.AllowAutoRedirect = null;\r\n        opts.MaxConnectionsPerServer = null;\r\n        var route = GivenDynamicRoute(opts);\r\n\r\n        // Act\r\n        var actual = _creator.Create(route, GlobalConfiguration());\r\n\r\n        // Assert\r\n        Assert.False(actual.AllowAutoRedirect);\r\n        Assert.Equal(111, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void CreateProtected_NullCheck()\r\n    {\r\n        // Arrange\r\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        IRouteGrouping grouping = null;\r\n        FileHttpHandlerOptions options = null;\r\n        FileGlobalHttpHandlerOptions globalOptions = null;\r\n\r\n        // Act\r\n        var wrapper = Assert.Throws<TargetInvocationException>(\r\n            () => method.Invoke(_creator, [grouping, options, globalOptions]));\r\n\r\n        // Assert\r\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\r\n        var actual = (ArgumentNullException)wrapper.InnerException;\r\n        Assert.Equal(nameof(grouping), actual.ParamName);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void CreateProtected()\r\n    {\r\n        // Arrange\r\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        FileDynamicRoute route = new() { Key = \"r1\" };\r\n        FileHttpHandlerOptions options = null;\r\n        var configuration = GlobalConfiguration();\r\n        FileGlobalHttpHandlerOptions globalOptions = configuration.HttpHandlerOptions;\r\n\r\n        // Act, Assert\r\n        var actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n        Assert.False(actual.AllowAutoRedirect); // global\r\n        Assert.Equal(111, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(111, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.False(actual.UseCookieContainer);\r\n        Assert.False(actual.UseProxy);\r\n        Assert.False(actual.UseTracing);\r\n\r\n        // Arrange 2\r\n        options = RouteOptions();\r\n        globalOptions.RouteKeys = [\"?\"];\r\n\r\n        // Act, Assert 2\r\n        actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n        Assert.True(actual.AllowAutoRedirect); // route\r\n        Assert.Equal(333, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n\r\n        globalOptions.RouteKeys = [\"r1\"];\r\n        actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n        Assert.True(actual.AllowAutoRedirect); // route\r\n        Assert.Equal(333, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n\r\n        globalOptions = null;\r\n        actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n        Assert.True(actual.AllowAutoRedirect); // route\r\n        Assert.Equal(333, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n\r\n        // Arrange 3\r\n        options.MaxConnectionsPerServer = null; // -> global\r\n        globalOptions = configuration.HttpHandlerOptions;\r\n        globalOptions.RouteKeys = null;\r\n        actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n        Assert.Equal(111, actual.MaxConnectionsPerServer); // global\r\n        Assert.True(actual.AllowAutoRedirect); // route\r\n        Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.True(actual.UseCookieContainer);\r\n        Assert.True(actual.UseProxy);\r\n        Assert.True(actual.UseTracing);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    public void CreateProtected_NoOptions()\r\n    {\r\n        // Arrange\r\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        FileDynamicRoute route = new();\r\n        FileHttpHandlerOptions options = null;\r\n        FileGlobalHttpHandlerOptions globalOptions = null;\r\n\r\n        // Act\r\n        var actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]);\r\n\r\n        // Assert : parameterless constructor was called\r\n        Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, actual.PooledConnectionLifeTime.TotalSeconds);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n\r\n    public void Merge_NullCheck()\r\n    {\r\n        // Arrange\r\n        var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic);\r\n        FileHttpHandlerOptions options = null;\r\n        FileHttpHandlerOptions globalOptions = null;\r\n\r\n        // Act, Assert 1\r\n        var wrapper = Assert.Throws<TargetInvocationException>(\r\n            () => method.Invoke(_creator, [null, globalOptions]));\r\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\r\n        var actual = (ArgumentNullException)wrapper.InnerException;\r\n        Assert.Equal(nameof(options), actual.ParamName);\r\n\r\n        // Act, Assert 2\r\n        options = new();\r\n        wrapper = Assert.Throws<TargetInvocationException>(\r\n            () => method.Invoke(_creator, [options, null]));\r\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\r\n        actual = (ArgumentNullException)wrapper.InnerException;\r\n        Assert.Equal(nameof(globalOptions), actual.ParamName);\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2320\")]\r\n    [Trait(\"PR\", \"2332\")] // https://github.com/ThreeMammals/Ocelot/pull/2332\r\n    [InlineData(false, true)]\r\n    [InlineData(true, true)]\r\n    [InlineData(false, false)]\r\n    [InlineData(true, false)]\r\n    public void Merge(bool isDef, bool hasTracer)\r\n    {\r\n        // Arrange\r\n        Arrange(hasTracer);\r\n        var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic);\r\n        FileHttpHandlerOptions options = new()\r\n        {\r\n            AllowAutoRedirect = isDef ? null : true,\r\n            MaxConnectionsPerServer = isDef ? null : 333,\r\n            PooledConnectionLifetimeSeconds = isDef ? null : 333,\r\n            UseCookieContainer = isDef ? null : true,\r\n            UseProxy = isDef ? null : true,\r\n            UseTracing = isDef ? null : true,\r\n        };\r\n        FileHttpHandlerOptions globalOptions = new()\r\n        {\r\n            AllowAutoRedirect = isDef ? null : false,\r\n            MaxConnectionsPerServer = isDef ? null : 111,\r\n            PooledConnectionLifetimeSeconds = isDef ? null : 111,\r\n            UseCookieContainer = isDef ? null : false,\r\n            UseProxy = isDef ? null : false,\r\n            UseTracing = isDef ? null : false,\r\n        };\r\n\r\n        // Act\r\n        var actual = (HttpHandlerOptions)method.Invoke(_creator, [options, globalOptions]);\r\n\r\n        // Assert\r\n        Assert.Equal(!isDef, actual.AllowAutoRedirect);\r\n        Assert.Equal(isDef ? int.MaxValue : 333, actual.MaxConnectionsPerServer);\r\n        Assert.Equal(isDef ? HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds : 333, actual.PooledConnectionLifeTime.TotalSeconds);\r\n        Assert.Equal(!isDef, actual.UseCookieContainer);\r\n        Assert.Equal(!isDef, actual.UseProxy);\r\n        Assert.Equal(hasTracer && !isDef, actual.UseTracing); // the useTracing parameter takes absolute priority\r\n    }\r\n\r\n    private static FileHttpHandlerOptions RouteOptions() => new()\r\n    {\r\n        AllowAutoRedirect = true,\r\n        MaxConnectionsPerServer = 333,\n        PooledConnectionLifetimeSeconds = 333,\n        UseCookieContainer = true,\n        UseProxy = true,\r\n        UseTracing = true,\n    };\r\n\r\n    private static FileGlobalConfiguration GlobalConfiguration() => new()\r\n    {\r\n        HttpHandlerOptions = new()\r\n        {\r\n            RouteKeys = null,\r\n            AllowAutoRedirect = false,\r\n            MaxConnectionsPerServer = 111,\r\n            PooledConnectionLifetimeSeconds = 111,\r\n            UseCookieContainer = false,\r\n            UseProxy = false,\r\n            UseTracing = false,\r\n        },\r\n    };\r\n\r\n    private static FileRoute GivenRoute(FileHttpHandlerOptions options = null)\r\n        => new() { HttpHandlerOptions = options ?? new() };\r\n    private static FileDynamicRoute GivenDynamicRoute(FileHttpHandlerOptions options = null)\r\n        => new() { HttpHandlerOptions = options ?? new() };\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class HttpHandlerOptionsTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        HttpHandlerOptions actual = new();\n\n        // Assert\n        Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer);\n        Assert.Equal(120, actual.PooledConnectionLifeTime.TotalSeconds);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Ctor_FileHttpHandlerOptions(bool isNull)\n    {\n        // Arrange\n        bool? nullBool() => isNull ? null : true;\n        int? nullInt() => isNull ? null : 123;\n        FileHttpHandlerOptions from = new()\n        {\n            AllowAutoRedirect = nullBool(),\n            MaxConnectionsPerServer = nullInt(),\n            PooledConnectionLifetimeSeconds = nullInt(),\n            UseCookieContainer = nullBool(),\n            UseProxy = nullBool(),\n            UseTracing = nullBool(),\n        };\n\n        // Act\n        HttpHandlerOptions actual = new(from);\n\n        // Assert\n        bool expectedBool = !isNull;\n        Assert.Equal(expectedBool, actual.AllowAutoRedirect);\n        Assert.Equal(isNull ? int.MaxValue : 123, actual.MaxConnectionsPerServer);\n        Assert.Equal(isNull ? 120 : 123, (int)actual.PooledConnectionLifeTime.TotalSeconds);\n        Assert.Equal(expectedBool, actual.UseCookieContainer);\n        Assert.Equal(expectedBool, actual.UseProxy);\n        Assert.Equal(expectedBool, actual.UseTracing);\n    }\n\n    [Fact]\n    public void Ctor_FileHttpHandlerOptions_MaxConnectionsPerServer()\n    {\n        // Arrange\n        FileHttpHandlerOptions from = new()\n        {\n            MaxConnectionsPerServer = null,\n        };\n\n        // Act, Assert\n        HttpHandlerOptions actual = new(from);\n        Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer);\n\n        from.MaxConnectionsPerServer = 0;\n        actual = new(from);\n        Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer);\n\n        from.MaxConnectionsPerServer = 111;\n        actual = new(from);\n        Assert.Equal(111, actual.MaxConnectionsPerServer);\n    }\n\n    [Theory]\n    [InlineData(false, true, false)]\n    [InlineData(true, null, false)]\n    [InlineData(true, false, false)]\n    [InlineData(true, true, true)]\n    public void Ctor_FileHttpHandlerOptions_bool(bool useTracing, bool? fromUseTracing, bool expected)\n    {\n        // Arrange\n        FileHttpHandlerOptions from = new()\n        {\n            MaxConnectionsPerServer = 333,\n            UseTracing = fromUseTracing,\n        };\n\n        // Act, Assert\n        HttpHandlerOptions actual = new(from, useTracing);\n\n        Assert.Equal(333, actual.MaxConnectionsPerServer);\n        Assert.Equal(expected, actual.UseTracing);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Creator;\n\nnamespace Ocelot.UnitTests.Configuration;\n\n[Trait(\"Feat\", \"1672\")]\npublic sealed class HttpVersionPolicyCreatorTests : UnitTest\n{\n    private readonly HttpVersionPolicyCreator _creator = new();\n\n    public HttpVersionPolicyCreatorTests()\n    {\n    }\n\n    [Theory]\n    [InlineData(VersionPolicies.RequestVersionOrLower, HttpVersionPolicy.RequestVersionOrLower)]\n    [InlineData(VersionPolicies.RequestVersionExact, HttpVersionPolicy.RequestVersionExact)]\n    [InlineData(VersionPolicies.RequestVersionOrHigher, HttpVersionPolicy.RequestVersionOrHigher)]\n    public void Should_create_version_policy_based_on_input(string versionPolicy, HttpVersionPolicy expected)\n    {\n        // Arrange, Act\n        var actual = _creator.Create(versionPolicy);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(null)]\n    [InlineData(\"invalid version\")]\n    public void Should_default_to_request_version_or_lower(string versionPolicy)\n    {\n        // Arrange, Act\n        var actual = _creator.Create(versionPolicy);\n\n        // Assert\n        Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual);\n    }\n\n    [Fact]\n    public void Should_default_to_request_version_or_lower_when_setting_gibberish()\n    {\n        // Arrange, Act\n        var actual = _creator.Create(\"string is gibberish\");\n\n        // Assert\n        Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/MetadataOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing System.Globalization;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class MetadataOptionsTests\n{\n    [Fact]\n    public void Ctor_Parameterless()\n    {\n        // Arrange, Act\n        MetadataOptions actual = new();\n\n        // Assert\n        Assert.Equal(CultureInfo.CurrentCulture, actual.CurrentCulture);\n        Assert.Equal(NumberStyles.Any, actual.NumberStyle);\n        Assert.Contains(\",\", actual.Separators);\n        Assert.Equal(StringSplitOptions.None, actual.StringSplitOption);\n        Assert.Contains(' ', actual.TrimChars);\n        Assert.NotNull(actual.Metadata);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    public void Ctor_CopyingFrom_MetadataOptions()\n    {\n        // Arrange\n        MetadataOptions from = new(\n            separators: [\"x\"],\n            trimChars: ['y'],\n            stringSplitOption: StringSplitOptions.TrimEntries,\n            numberStyle: NumberStyles.Number,\n            currentCulture: CultureInfo.GetCultureInfo(\"uk\"),\n            metadata: new Dictionary<string, string>()\n            {\n                { \"key\", \"value\" },\n            });\n\n        // Act\n        MetadataOptions actual = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    public void Ctor_CopyingFrom_FileMetadataOptions()\n    {\n        // Arrange\n        FileMetadataOptions from = new()\n        {\n            CurrentCulture = \"uk\",\n            NumberStyle = nameof(NumberStyles.Number),\n            Separators = [\"x\"],\n            StringSplitOption = nameof(StringSplitOptions.RemoveEmptyEntries),\n            TrimChars = [';'],\n        };\n\n        // Act\n        MetadataOptions actual = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equal(CultureInfo.GetCultureInfo(\"uk\"), actual.CurrentCulture);\n        Assert.Equal(NumberStyles.Number, actual.NumberStyle);\n        Assert.Contains(\"x\", actual.Separators);\n        Assert.Equal(StringSplitOptions.RemoveEmptyEntries, actual.StringSplitOption);\n        Assert.Contains(';', actual.TrimChars);\n        Assert.NotNull(actual.Metadata);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Parser/ParsingConfigurationHeaderErrorTests.cs",
    "content": "﻿using Ocelot.Configuration.Parser;\nusing Ocelot.Errors;\n\nnamespace Ocelot.UnitTests.Configuration.Parser;\n\npublic class ParsingConfigurationHeaderErrorTests : UnitTest\n{\n    [Fact]\n    public void Ctor()\n    {\n        Exception exception = new(TestID);\n        ParsingConfigurationHeaderError error = new(exception);\n\n        Assert.Equal($\"Parsing configuration exception is {TestID}\", error.Message);\n        Assert.Equal(OcelotErrorCode.ParsingConfigurationHeaderError, error.Code);\n        Assert.Equal(404, error.HttpStatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Repository/ConsulFileConfigurationPollerOptionTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Configuration.Repository;\n\npublic class ConsulFileConfigurationPollerOptionTests\n{\n    private readonly Mock<IInternalConfigurationRepository> _mockInternalConfigRepo = new();\n    private readonly Mock<IFileConfigurationRepository> _mockFileConfigurationRepository = new();\n    private readonly ConsulFileConfigurationPollerOption _sut; // System Under Test\n\n    public ConsulFileConfigurationPollerOptionTests()\n    {\n        _sut = new(\n            _mockInternalConfigRepo.Object,\n            _mockFileConfigurationRepository.Object);\n    }\n\n    [Fact]\n    public void Constructor_ShouldSetDependencies()\n    {\n        // Arrange & Act\n        var result = _sut;\n\n        // Assert\n        Assert.NotNull(result);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenFileConfigurationIsNull()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnFileConfigPollingInterval_WhenFileConfigHasValidPollingInterval()\n    {\n        // Arrange\n        const int expectedDelay = 5000;\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = expectedDelay\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(expectedDelay, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenFileConfigPollingIntervalIsZero()\n    {\n        // Arrange\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = 0\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenFileConfigIsError()\n    {\n        // Arrange\n        var err = new UnableToSetConfigInConsulError(\"Error message\");\n        var fileConfigResponse = new ErrorResponse<FileConfiguration>(err);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenFileConfigServiceDiscoveryProviderIsNull()\n    {\n        // Arrange\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = null\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnInternalConfigPollingInterval_WhenFileConfigFailsButInternalConfigIsValid()\n    {\n        // Arrange\n        const int expectedDelay = 3000;\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfiguration = new InternalConfiguration\n        {\n            ServiceProviderConfiguration = new()\n            {\n                PollingInterval = expectedDelay,\n            }\n        };\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(internalConfiguration);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(expectedDelay, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenInternalConfigPollingIntervalIsZero()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfiguration = new InternalConfiguration\n        {\n            ServiceProviderConfiguration = new()\n            {\n                PollingInterval = 0,\n            }\n        };\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(internalConfiguration);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenInternalConfigIsError()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var err = new UnableToSetConfigInConsulError(\"Error message\");\n        var internalConfigResponse = new ErrorResponse<IInternalConfiguration>(err);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturnDefaultValue_WhenInternalConfigServiceProviderConfigurationIsNull()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfiguration = new InternalConfiguration\n        {\n            ServiceProviderConfiguration = null\n        };\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(internalConfiguration);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldPreferFileConfigOverInternalConfig_WhenBothHaveValidPollingIntervals()\n    {\n        // Arrange\n        const int fileConfigDelay = 5000;\n        const int internalConfigDelay = 3000;\n\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = fileConfigDelay,\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfiguration = new InternalConfiguration\n        {\n            ServiceProviderConfiguration = new ServiceProviderConfiguration\n            {\n                PollingInterval = internalConfigDelay\n            }\n        };\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(internalConfiguration);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(fileConfigDelay, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldReturn1000_WhenPollingIntervalIsNegative()\n    {\n        // Arrange\n        const int negativeDelay = -100;\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = negativeDelay,\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        // Note: The current implementation allows negative values to pass through\n        // This test documents current behavior; consider if validation is needed\n        Assert.Equal(1000, delay);\n    }\n\n    [Fact]\n    public void Delay_ShouldCallFileConfigurationRepositoryGet()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        _mockFileConfigurationRepository.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void Delay_ShouldCallInternalConfigRepositoryGet_WhenFileConfigDoesNotHaveValidPollingInterval()\n    {\n        // Arrange\n        var fileConfigResponse = new OkResponse<FileConfiguration>(null);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        var internalConfigResponse = new OkResponse<IInternalConfiguration>(null);\n        _mockInternalConfigRepo\n            .Setup(x => x.Get())\n            .Returns(internalConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        _mockInternalConfigRepo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public void Delay_ShouldNotCallInternalConfigRepositoryGet_WhenFileConfigHasValidPollingInterval()\n    {\n        // Arrange\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = 5000,\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        _mockInternalConfigRepo.Verify(x => x.Get(), Times.Never);\n    }\n\n    [Theory]\n    [InlineData(100)]\n    [InlineData(1000)]\n    [InlineData(5000)]\n    [InlineData(10000)]\n    public void Delay_ShouldReturnValidPollingInterval_WithVariousValues(int pollingInterval)\n    {\n        // Arrange\n        var fileConfiguration = new FileConfiguration\n        {\n            GlobalConfiguration = new()\n            {\n                ServiceDiscoveryProvider = new()\n                {\n                    PollingInterval = pollingInterval\n                }\n            }\n        };\n        var fileConfigResponse = new OkResponse<FileConfiguration>(fileConfiguration);\n        _mockFileConfigurationRepository\n            .Setup(x => x.Get())\n            .ReturnsAsync(fileConfigResponse);\n\n        // Act\n        var delay = _sut.Delay;\n\n        // Assert\n        Assert.Equal(pollingInterval, delay);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Repository/DiskFileConfigurationRepositoryTests.cs",
    "content": "using Microsoft.AspNetCore.Hosting;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.DependencyInjection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.Configuration.Repository;\n\npublic sealed class DiskFileConfigurationRepositoryTests : FileUnitTest\n{\n    private readonly Mock<IWebHostEnvironment> _hostingEnvironment;\n    private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;\n    private DiskFileConfigurationRepository _repo;\n    private FileConfiguration _result;\n\n    public DiskFileConfigurationRepositoryTests()\n    {\n        _hostingEnvironment = new Mock<IWebHostEnvironment>();\n        _changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);\n        _changeTokenSource.Setup(m => m.Activate());\n    }\n\n    private void Arrange([CallerMemberName] string testName = null)\n    {\n        _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(testName);\n        _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, TestID);\n    }\n\n    [Fact]\n    public async Task Should_return_file_configuration()\n    {\n        Arrange();\n        var config = FakeFileConfigurationForGet();\n        GivenTheConfigurationIs(config);\n\n        // Act\n        _result = (await _repo.Get()).Data;\n\n        // Assert\n        ThenTheFollowingIsReturned(config);\n    }\n\n    [Fact]\n    public async Task Should_return_file_configuration_if_environment_name_is_unavailable()\n    {\n        Arrange();\n        var config = FakeFileConfigurationForGet();\n        GivenTheEnvironmentNameIsUnavailable();\n        GivenTheConfigurationIs(config);\n\n        // Act\n        _result = (await _repo.Get()).Data;\n\n        // Assert\n        ThenTheFollowingIsReturned(config);\n    }\n\n    [Fact]\n    public async Task Should_set_file_configuration()\n    {\n        Arrange();\n        var config = FakeFileConfigurationForSet();\n\n        // Act\n        await WhenISetTheConfiguration(config);\n\n        // Assert\n        ThenTheConfigurationIsStoredAs(config);\n        ThenTheConfigurationJsonIsIndented(config);\n        _changeTokenSource.Verify(m => m.Activate(), Times.Once); // and the change token is activated\n    }\n\n    [Fact]\n    public async Task Should_set_file_configuration_if_environment_name_is_unavailable()\n    {\n        Arrange();\n        var config = FakeFileConfigurationForSet();\n        GivenTheEnvironmentNameIsUnavailable();\n\n        // Act\n        await WhenISetTheConfiguration(config);\n\n        // Assert\n        ThenTheConfigurationIsStoredAs(config);\n        ThenTheConfigurationJsonIsIndented(config);\n    }\n\n    [Fact]\n    public async Task Should_set_environment_file_configuration_and_ocelot_file_configuration()\n    {\n        Arrange();\n        var config = FakeFileConfigurationForSet();\n        GivenTheConfigurationIs(config);\n        var ocelotJson = GivenTheUserAddedOcelotJson();\n\n        // Act\n        await WhenISetTheConfiguration(config);\n\n        // Assert\n        ThenTheConfigurationIsStoredAs(config);\n        ThenTheConfigurationJsonIsIndented(config);\n        ThenTheOcelotJsonIsStoredAs(ocelotJson, config);\n    }\n\n    private FileInfo GivenTheUserAddedOcelotJson()\n    {\n        var primaryFile = Path.Combine(TestID, ConfigurationBuilderExtensions.PrimaryConfigFile);\n        var ocelotJson = new FileInfo(primaryFile);\n        if (ocelotJson.Exists)\n        {\n            ocelotJson.Delete();\n        }\n\n        File.WriteAllText(ocelotJson.FullName, \"Doesnt matter\");\n        files.Add(ocelotJson.FullName);\n        return ocelotJson;\n    }\n\n    private void GivenTheEnvironmentNameIsUnavailable()\n    {\n        _hostingEnvironment.Setup(he => he.EnvironmentName).Returns((string)null);\n    }\n\n    private async Task WhenISetTheConfiguration(FileConfiguration fileConfiguration)\n    {\n        await _repo.Set(fileConfiguration);\n        var response = await _repo.Get();\n        _result = response.Data;\n    }\n\n    private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds)\n    {\n        _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port);\n\n        for (var i = 0; i < _result.Routes.Count; i++)\n        {\n            for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++)\n            {\n                var result = _result.Routes[i].DownstreamHostAndPorts[j];\n                var expected = expecteds.Routes[i].DownstreamHostAndPorts[j];\n\n                result.Host.ShouldBe(expected.Host);\n                result.Port.ShouldBe(expected.Port);\n            }\n\n            _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate);\n            _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme);\n        }\n    }\n\n    private static void ThenTheOcelotJsonIsStoredAs(FileInfo ocelotJson, FileConfiguration expecteds)\n    {\n        var actual = File.ReadAllText(ocelotJson.FullName);\n        var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented);\n        actual.ShouldBe(expectedText);\n    }\n\n    private void GivenTheConfigurationIs(FileConfiguration fileConfiguration, [CallerMemberName] string environmentName = null)\n    {\n        var environmentSpecificPath = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName));\n        var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);\n        var environmentSpecific = new FileInfo(environmentSpecificPath);\n        if (environmentSpecific.Exists)\n        {\n            environmentSpecific.Delete();\n        }\n\n        File.WriteAllText(environmentSpecific.FullName, jsonConfiguration);\n        files.Add(environmentSpecific.FullName);\n    }\n\n    private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds, [CallerMemberName] string environmentName = null)\n    {\n        var environmentSpecific = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName));\n        var actual = File.ReadAllText(environmentSpecific);\n        var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented);\n        actual.ShouldBe(expectedText);\n        files.Add(environmentSpecific);\n    }\n\n    private void ThenTheFollowingIsReturned(FileConfiguration expecteds)\n    {\n        _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host);\n        _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port);\n\n        for (var i = 0; i < _result.Routes.Count; i++)\n        {\n            for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++)\n            {\n                var result = _result.Routes[i].DownstreamHostAndPorts[j];\n                var expected = expecteds.Routes[i].DownstreamHostAndPorts[j];\n\n                result.Host.ShouldBe(expected.Host);\n                result.Port.ShouldBe(expected.Port);\n            }\n\n            _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate);\n            _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme);\n        }\n    }\n\n    private static FileConfiguration FakeFileConfigurationForSet()\n    {\n        var route = GivenRoute(\"123.12.12.12\", \"/asdfs/test/{test}\");\n        return GivenConfiguration(route);\n    }\n\n    private static FileConfiguration FakeFileConfigurationForGet()\n    {\n        var route = GivenRoute(\"localhost\", \"/test/test/{test}\");\n        return GivenConfiguration(route);\n    }\n\n    private static FileRoute GivenRoute(string host, string downstream) => new()\n    {\n        DownstreamHostAndPorts = new() { new(host, 80) },\n        DownstreamScheme = Uri.UriSchemeHttps,\n        DownstreamPathTemplate = downstream,\n    };\n\n    private static FileConfiguration GivenConfiguration(params FileRoute[] routes)\n    {\n        var config = new FileConfiguration();\n        config.Routes.AddRange(routes);\n        config.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = \"https\",\n            Port = 198,\n            Host = \"blah\",\n        };\n        return config;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Repository/FileConfigurationPollerTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\nnamespace Ocelot.UnitTests.Configuration.Repository;\n\npublic sealed class FileConfigurationPollerTests : UnitTest, IDisposable\n{\n    private const int PollingDelayInMs = 100;\n    private const int LongRunningPollDelayInMs = PollingDelayInMs + 50;\n\n    private readonly FileConfigurationPoller _poller;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IFileConfigurationRepository> _repo;\n    private readonly FileConfiguration _initialFileConfig;\n    private readonly Mock<IFileConfigurationPollerOptions> _config;\n    private readonly Mock<IInternalConfigurationRepository> _internalConfigRepo;\n    private readonly Mock<IInternalConfigurationCreator> _internalConfigCreator;\n    private readonly Mock<IInternalConfiguration> _internalConfig;\n\n    public FileConfigurationPollerTests()\n    {\n        var logger = new Mock<IOcelotLogger>();\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _factory.Setup(x => x.CreateLogger<FileConfigurationPoller>()).Returns(logger.Object);\n        _repo = new Mock<IFileConfigurationRepository>();\n        _initialFileConfig = new FileConfiguration();\n        _config = new Mock<IFileConfigurationPollerOptions>();\n        _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse<FileConfiguration>(_initialFileConfig));\n        _config.Setup(x => x.Delay).Returns(PollingDelayInMs);\n        _internalConfig = new Mock<IInternalConfiguration>();\n        _internalConfigRepo = new Mock<IInternalConfigurationRepository>();\n        _internalConfigCreator = new Mock<IInternalConfigurationCreator>();\n        _internalConfigCreator.Setup(x => x.Create(It.IsAny<FileConfiguration>())).ReturnsAsync(new OkResponse<IInternalConfiguration>(_internalConfig.Object));\n        _poller = new FileConfigurationPoller(_factory.Object, _repo.Object, _config.Object, _internalConfigRepo.Object, _internalConfigCreator.Object);\n    }\n\n    [Fact]\n    public void Should_start_and_poll_initial_configuration()\n    {\n        // Arrange, Act\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        ThenTheSetterIsCalled(_initialFileConfig, 1);\n    }\n\n    [Fact]\n    public async Task Should_not_replace_timer_when_start_called_twice()\n    {\n        // Arrange\n        await _poller.StartAsync(TestContext.Current.CancellationToken);\n        var timerAfterFirstStart = CurrentTimer();\n\n        // Act\n        await _poller.StartAsync(TestContext.Current.CancellationToken);\n        var timerAfterSecondStart = CurrentTimer();\n\n        // Assert\n        timerAfterFirstStart.ShouldNotBeNull();\n        timerAfterSecondStart.ShouldBeSameAs(timerAfterFirstStart);\n    }\n\n    [Fact]\n    public async Task Should_do_nothing_when_stop_called_before_start()\n    {\n        // Arrange, Act\n        await _poller.StopAsync(TestContext.Current.CancellationToken);\n        await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken);\n\n        // Assert\n        NumberOfGetInvocations().ShouldBe(0);\n    }\n\n    [Fact]\n    public void Should_call_setter_when_gets_new_config()\n    {\n        // Arrange\n        var newConfig = GivenConfiguration();\n\n        // Act\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        WhenTheConfigIsChanged(newConfig, 0);\n        ThenTheSetterIsCalled(newConfig, 1);\n    }\n\n    [Fact]\n    public void Should_call_setter_only_once_when_configuration_does_not_change_across_multiple_poll_cycles()\n    {\n        // Arrange, Act\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        ThenTheSetterIsCalled(_initialFileConfig, 1);\n        ThenTheConfigIsNotAddedMoreThan(1);\n    }\n\n    [Fact]\n    public void Should_not_poll_if_already_polling()\n    {\n        // Arrange\n        var newConfig = GivenConfiguration();\n\n        // Act\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        WhenTheConfigIsChanged(newConfig, LongRunningPollDelayInMs);\n        ThenTheSetterIsCalled(newConfig, 1);\n    }\n\n    [Fact]\n    public async Task Should_return_early_on_timer_tick_when_polling_is_already_in_progress()\n    {\n        // Arrange\n        var getTaskSource = new TaskCompletionSource<Response<FileConfiguration>>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var getCallCount = 0;\n        _repo.Setup(x => x.Get()).Returns(() =>\n        {\n            Interlocked.Increment(ref getCallCount);\n            return getTaskSource.Task;\n        });\n\n        // Act\n        await _poller.StartAsync(TestContext.Current.CancellationToken);\n        await Task.Delay(PollingDelayInMs * 3, TestContext.Current.CancellationToken);\n\n        // Assert\n        getCallCount.ShouldBe(1);\n\n        // Cleanup\n        getTaskSource.SetResult(new OkResponse<FileConfiguration>(_initialFileConfig));\n        await _poller.StopAsync(TestContext.Current.CancellationToken);\n    }\n\n    [Fact]\n    public void Should_do_nothing_if_call_to_provider_fails()\n    {\n        // Arrange, Act\n        WhenProviderErrors();\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        ThenTheProviderIsPolled();\n        ThenTheSetterIsNotCalled();\n    }\n\n    [Fact]\n    public void Should_not_add_to_internal_repo_if_internal_configuration_creation_fails()\n    {\n        // Arrange\n        var newConfig = GivenConfiguration();\n\n        _internalConfigCreator\n            .Setup(x => x.Create(It.IsAny<FileConfiguration>()))\n            .ReturnsAsync(new ErrorResponse<IInternalConfiguration>(new AnyError()));\n        _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse<FileConfiguration>(newConfig));\n\n        // Act\n        _poller.StartAsync(CancellationToken.None);\n\n        // Assert\n        ThenTheCreatorIsCalled(newConfig, 1);\n        ThenTheConfigIsNotAdded();\n    }\n\n    [Fact]\n    public async Task Should_stop_polling_when_stopped()\n    {\n        // Arrange, Act\n        await _poller.StartAsync(TestContext.Current.CancellationToken);\n        await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken);\n        await _poller.StopAsync(TestContext.Current.CancellationToken);\n        await Task.Delay(PollingDelayInMs, TestContext.Current.CancellationToken);\n        var afterStopSettled = NumberOfGetInvocations();\n        await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken);\n\n        // Assert\n        ThenTheSetterIsCalled(_initialFileConfig, 1);\n        NumberOfGetInvocations().ShouldBe(afterStopSettled);\n    }\n\n    [Fact]\n    public void Should_dispose_cleanly_without_starting()\n    {\n        // Arrange, Act, Assert\n        _poller.Dispose(); // when poller is disposed\n    }\n\n    private static FileConfiguration GivenConfiguration() => new()\n    {\n        Routes = new()\n        {\n            new()\n            {\n                DownstreamHostAndPorts = [ new(\"test\", 80) ],\n            },\n        },\n    };\n\n    private void WhenProviderErrors()\n    {\n        _repo\n            .Setup(x => x.Get())\n            .ReturnsAsync(new ErrorResponse<FileConfiguration>(new AnyError()));\n    }\n\n    private void WhenTheConfigIsChanged(FileConfiguration newConfig, int delay)\n    {\n        _repo\n            .Setup(x => x.Get())\n            .Callback(() => Thread.Sleep(delay))\n            .ReturnsAsync(new OkResponse<FileConfiguration>(newConfig));\n    }\n\n    private bool AssertWhile(Action assertion, int milliSeconds = 4_000)\n    {\n        bool TryAssert()\n        {\n            try\n            {\n                assertion.Invoke();\n                return true;\n            }\n            catch (Exception)\n            {\n                return false;\n            }\n        }\n        return Wait.For(milliSeconds).Until(TryAssert);\n    }\n\n    private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times)\n    {\n        var result = AssertWhile(() =>\n        {\n            _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.Exactly(times));\n            _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times));\n        });\n        Assert.True(result);\n    }\n\n    private void ThenTheSetterIsNotCalled()\n    {\n        _internalConfigRepo.Verify(x => x.AddOrReplace(It.IsAny<IInternalConfiguration>()), Times.Never);\n        _internalConfigCreator.Verify(x => x.Create(It.IsAny<FileConfiguration>()), Times.Never);\n    }\n\n    private void ThenTheCreatorIsCalled(FileConfiguration fileConfig, int times)\n    {\n        var result = AssertWhile(() =>\n        {\n            _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times));\n        });\n        Assert.True(result);\n    }\n\n    private void ThenTheCreatorIsCalled(int times)\n    {\n        var result = AssertWhile(() =>\n        {\n            _internalConfigCreator.Verify(x => x.Create(It.IsAny<FileConfiguration>()), Times.Exactly(times));\n        });\n        Assert.True(result);\n    }\n\n    private void ThenTheConfigIsNotAdded()\n    {\n        _internalConfigRepo.Verify(x => x.AddOrReplace(It.IsAny<IInternalConfiguration>()), Times.Never);\n    }\n\n    private void ThenTheConfigIsNotAddedMoreThan(int times)\n    {\n        var result = AssertWhile(() =>\n        {\n            _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.Exactly(times));\n        });\n        Assert.True(result);\n    }\n\n    private int NumberOfGetInvocations()\n    {\n        return _repo.Invocations.Count(x => x.Method.Name == nameof(IFileConfigurationRepository.Get));\n    }\n\n    private Timer CurrentTimer()\n    {\n        var timerField = typeof(FileConfigurationPoller)\n            .GetField(\"_timer\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);\n\n        timerField.ShouldNotBeNull();\n        return timerField.GetValue(_poller) as Timer;\n    }\n\n    private void ThenTheProviderIsPolled()\n    {\n        var result = AssertWhile(() =>\n        {\n            _repo.Verify(x => x.Get(), Times.AtLeastOnce());\n        });\n        Assert.True(result);\n    }\n\n    public void Dispose()\n    {\n        _poller.Dispose();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Repository/InMemoryConfigurationRepositoryTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.ChangeTracking;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Configuration.Repository;\n\npublic class InMemoryConfigurationRepositoryTests : UnitTest\n{\n    private readonly InMemoryInternalConfigurationRepository _repo;\n    private IInternalConfiguration _config;\n    private Response _result;\n    private Response<IInternalConfiguration> _getResult;\n    private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;\n\n    public InMemoryConfigurationRepositoryTests()\n    {\n        _changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);\n        _changeTokenSource.Setup(m => m.Activate());\n        _repo = new InMemoryInternalConfigurationRepository(_changeTokenSource.Object);\n    }\n\n    [Fact]\n    public void Can_add_config()\n    {\n        // Arrange\n        _config = new FakeConfig(\"initial\", \"adminath\");\n\n        // Act\n        _result = _repo.AddOrReplace(_config);\n\n        // Assert\n        _result.IsError.ShouldBeFalse();\n        _changeTokenSource.Verify(m => m.Activate(), Times.Once);\n    }\n\n    [Fact]\n    public void Can_get_config()\n    {\n        // Arrange\n        _config = new FakeConfig(\"initial\", \"adminath\");\n        _result = _repo.AddOrReplace(_config);\n\n        // Act\n        _getResult = _repo.Get();\n\n        // Assert\n        _getResult.Data.Routes[0].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(\"initial\");\n    }\n\n    private class FakeConfig : IInternalConfiguration\n    {\n        private readonly string _downstreamTemplatePath;\n\n        public FakeConfig(string downstreamTemplatePath, string administrationPath)\n        {\n            _downstreamTemplatePath = downstreamTemplatePath;\n            AdministrationPath = administrationPath;\n        }\n\n        public Route[] Routes\n        {\n            get\n            {\n                var downstreamRoute = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(_downstreamTemplatePath)\n                    .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n                    .Build();\n\n                return new Route[]\n                {\n                    new(downstreamRoute, HttpMethod.Get),\n                };\n            }\n        }\n\n        public string AdministrationPath { get; }\n        public AuthenticationOptions AuthenticationOptions { get; }\n        public ServiceProviderConfiguration ServiceProviderConfiguration => throw new NotImplementedException();\n        public string RequestId { get; }\n        public LoadBalancerOptions LoadBalancerOptions { get; }\n        public string DownstreamScheme { get; }\n        public QoSOptions QoSOptions { get; }\n        public HttpHandlerOptions HttpHandlerOptions { get; }\n        public Version DownstreamHttpVersion { get; }\n        public HttpVersionPolicy DownstreamHttpVersionPolicy { get; }\n        public MetadataOptions MetadataOptions => throw new NotImplementedException();\n        public RateLimitOptions RateLimitOptions => throw new NotImplementedException();\n        public int? Timeout => throw new NotImplementedException();\n        public CacheOptions CacheOptions => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Repository/InMemoryFileConfigurationPollerOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration.Repository;\n\nnamespace Ocelot.UnitTests.Configuration.Repository;\n\npublic class InMemoryFileConfigurationPollerOptionsTests\n{\n    [Fact]\n    public void Delay()\n    {\n        InMemoryFileConfigurationPollerOptions sut = new();\n\n        Assert.Equal(1000, sut.Delay);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs",
    "content": "using Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class RequestIdKeyCreatorTests : UnitTest\r\n{\r\n    private readonly RequestIdKeyCreator _creator = new();\r\n\r\n    [Fact]\r\n    public void Should_use_global_configuration()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute();\r\n        var globalConfig = new FileGlobalConfiguration\r\n        {\r\n            RequestIdKey = \"cheese\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(route, globalConfig);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"cheese\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_re_route_specific()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            RequestIdKey = \"cheese\",\r\n        };\r\n        var globalConfig = new FileGlobalConfiguration();\r\n\r\n        // Act\r\n        var result = _creator.Create(route, globalConfig);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"cheese\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_re_route_over_global_specific()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            RequestIdKey = \"cheese\",\r\n        };\r\n        var globalConfig = new FileGlobalConfiguration\r\n        {\r\n            RequestIdKey = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(route, globalConfig);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"cheese\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs",
    "content": "using Ocelot.Configuration;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.LoadBalancer.Balancers;\r\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class RouteKeyCreatorTests : UnitTest\r\n{\r\n    private readonly RouteKeyCreator _creator = new();\r\n\r\n    [Fact]\r\n    public void Should_return_sticky_session_key()\r\n    {\r\n        // Arrange\r\n        FileRoute route = new();\r\n        LoadBalancerOptions options = new(nameof(CookieStickySessions), \"testy\", null);\r\n\r\n        // Act\r\n        var result = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"CookieStickySessions:testy\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_key()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/product\",\r\n            UpstreamHttpMethod = [\"GET\", \"POST\", \"PUT\"],\r\n            DownstreamHostAndPorts = new()\r\n            {\r\n                new(\"localhost\", 8080),\r\n                new(\"localhost\", 4430),\r\n            },\r\n        };\r\n        LoadBalancerOptions options = new();\r\n\r\n        // Act\r\n        var result = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_key_with_upstream_host()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamHost = \"my-host\",\r\n            UpstreamPathTemplate = \"/api/product\",\r\n            UpstreamHttpMethod = [\"GET\", \"POST\", \"PUT\"],\r\n            DownstreamHostAndPorts = new()\r\n            {\r\n                new(\"localhost\", 8080),\r\n                new(\"localhost\", 4430),\r\n            },\r\n        };\r\n        LoadBalancerOptions options = new();\r\n\r\n        // Act\r\n        var result = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_key_with_svc_name()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/product\",\r\n            UpstreamHttpMethod = [\"GET\", \"POST\", \"PUT\"],\r\n            ServiceName = \"products-service\",\r\n        };\r\n        LoadBalancerOptions options = new();\r\n\r\n        // Act\r\n        var result = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|NoLoadBalancer|no-lb-key\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_key_with_load_balancer_options()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/product\",\r\n            UpstreamHttpMethod = [\"GET\", \"POST\", \"PUT\"],\r\n            ServiceName = \"products-service\",\r\n            LoadBalancerOptions = new FileLoadBalancerOptions\r\n            {\r\n                Type = nameof(LeastConnection),\r\n                Key = \"testy\",\r\n            },\r\n        };\r\n        LoadBalancerOptions options = new(route.LoadBalancerOptions);\r\n\r\n        // Act\r\n        var result = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        result.ShouldBe(\"GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|LeastConnection|testy\");\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_FileDynamicRoute_TryStickySession()\r\n    {\r\n        // Arrange\r\n        FileDynamicRoute route = new();\r\n        LoadBalancerOptions options = new(nameof(CookieStickySessions), \"TestKey\", null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"CookieStickySessions:TestKey\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_FileDynamicRoute_HasLoadBalancingKey()\r\n    {\r\n        // Arrange\r\n        FileDynamicRoute route = new();\r\n        LoadBalancerOptions options = new(nameof(RoundRobin), \"LBKey\", null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"LBKey\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_FileDynamicRoute_NoLBKey()\r\n    {\r\n        // Arrange\r\n        FileDynamicRoute route = new()\r\n        {\r\n            ServiceName = \"test\",\r\n            ServiceNamespace = \"namespace\",\r\n        };\r\n        LoadBalancerOptions options = new(nameof(RoundRobin), null, null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(route, options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"namespace.test\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_String_String_TryStickySession()\r\n    {\r\n        // Arrange\r\n        LoadBalancerOptions options = new(nameof(CookieStickySessions), \"TestKey\", null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(\"namespace\", \"service\", options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"CookieStickySessions:TestKey\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_String_String_HasLoadBalancingKey()\r\n    {\r\n        // Arrange\r\n        LoadBalancerOptions options = new(nameof(RoundRobin), \"LBKey\", null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(\"namespace\", \"service\", options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"LBKey\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void Create_String_String_NoLBKey()\r\n    {\r\n        // Arrange\r\n        LoadBalancerOptions options = new(nameof(RoundRobin), null, null);\r\n\r\n        // Act\r\n        var actual = _creator.Create(\"namespace\", \"service\", options);\r\n\r\n        // Assert\r\n        Assert.Equal(\"namespace.service\", actual);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")]\r\n    public void AsString()\r\n    {\r\n        // Arrange, Act, Assert\r\n        FileHostAndPort host = null;\r\n        var actual = RouteKeyCreator.AsString(host);\r\n        Assert.Null(actual);\r\n\r\n        // Arrange, Act, Assert\r\n        host = new(\"test.host\", 123);\r\n        actual = RouteKeyCreator.AsString(host);\r\n        Assert.Equal(\"test.host:123\", actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/RouteTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class RouteTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        Route r = new();\n\n        // Assert\n        Assert.NotNull(r.DownstreamRoute);\n        Assert.Empty(r.DownstreamRoute);\n\n        Assert.False(r.IsDynamic);\n        Assert.Null(r.Aggregator);\n        Assert.NotNull(r.DownstreamRoute);\n        Assert.Null(r.DownstreamRouteConfig);\n        Assert.Null(r.UpstreamHeaderTemplates);\n        Assert.Null(r.UpstreamHost);\n        Assert.Null(r.UpstreamHttpMethod);\n        Assert.Null(r.UpstreamTemplatePattern);\n    }\n\n    [Fact]\n    public void Ctor_Boolean()\n    {\n        // Arrange, Act\n        Route r1 = new(true),\n            r2 = new(false);\n\n        Assert.True(r1.IsDynamic);\n        Assert.False(r2.IsDynamic);\n        Assert.Empty(r1.DownstreamRoute);\n        Assert.Empty(r2.DownstreamRoute);\n    }\n\n    [Fact]\n    public void Ctor_Boolean_DownstreamRoute()\n    {\n        // Arrange\n        DownstreamRoute route = new DownstreamRouteBuilder().Build();\n\n        // Act\n        Route r = new(true, route);\n\n        Assert.True(r.IsDynamic);\n        Assert.NotEmpty(r.DownstreamRoute);\n        Assert.Equal(route, r.DownstreamRoute[0]);\n    }\n\n    [Fact]\n    public void Ctor_DownstreamRoute()\n    {\n        // Arrange\n        DownstreamRoute route = new DownstreamRouteBuilder().Build();\n\n        // Act\n        Route r = new(route);\n\n        Assert.False(r.IsDynamic);\n        Assert.NotEmpty(r.DownstreamRoute);\n        Assert.Equal(route, r.DownstreamRoute[0]);\n    }\n\n    [Fact]\n    public void Ctor_DownstreamRoute_HttpMethod()\n    {\n        // Arrange\n        DownstreamRoute route = new DownstreamRouteBuilder().Build();\n        HttpMethod method = HttpMethod.Connect;\n\n        // Act\n        Route r = new(route, method);\n\n        Assert.NotEmpty(r.DownstreamRoute);\n        Assert.Equal(route, r.DownstreamRoute[0]);\n        Assert.NotEmpty(r.UpstreamHttpMethod);\n        Assert.Equal(method, r.UpstreamHttpMethod.First());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic sealed class SecurityOptionsCreatorTests : UnitTest\r\n{\r\n    private readonly SecurityOptionsCreator _creator = new();\r\n\r\n    [Fact]\r\n    public void Should_create_route_security_config()\r\n    {\r\n        // Arrange\r\n        var ipAllowedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var ipBlockedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var securityOptions = new FileSecurityOptions\r\n        {\r\n            IPAllowedList = ipAllowedList,\r\n            IPBlockedList = ipBlockedList,\r\n        };\r\n        var expected = new SecurityOptions(ipAllowedList, ipBlockedList);\r\n        var globalConfig = new FileGlobalConfiguration();\r\n\r\n        // Act\r\n        var actual = _creator.Create(securityOptions, globalConfig);\r\n\r\n        // Assert\r\n        actual.ThenTheResultIs(expected);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"2170\")]\r\n    public void Should_create_global_security_config()\r\n    {\r\n        // Arrange\r\n        var ipAllowedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var ipBlockedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var globalConfig = new FileGlobalConfiguration\r\n        {\r\n            SecurityOptions = new()\r\n            {\r\n                IPAllowedList = ipAllowedList,\r\n                IPBlockedList = ipBlockedList,\r\n            },\r\n        };\r\n        var expected = new SecurityOptions(ipAllowedList, ipBlockedList);\r\n\r\n        // Act\r\n        var actual = _creator.Create(new(), globalConfig);\r\n\r\n        // Assert\r\n        actual.ThenTheResultIs(expected);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"2170\")]\r\n    public void Should_create_global_route_security_config()\r\n    {\r\n        // Arrange\r\n        var routeIpAllowedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var routeIpBlockedList = new List<string> { \"127.0.0.1\", \"192.168.1.1\" };\r\n        var securityOptions = new FileSecurityOptions\r\n        {\r\n            IPAllowedList = routeIpAllowedList,\r\n            IPBlockedList = routeIpBlockedList,\r\n        };\r\n        var globalIpAllowedList = new List<string> { \"127.0.0.2\", \"192.168.1.2\" };\r\n        var globalIpBlockedList = new List<string> { \"127.0.0.2\", \"192.168.1.2\" };\r\n        var globalConfig = new FileGlobalConfiguration\r\n        {\r\n            SecurityOptions = new FileSecurityOptions\r\n            {\r\n                IPAllowedList = globalIpAllowedList,\r\n                IPBlockedList = globalIpBlockedList,\r\n            },\r\n        };\r\n        var expected = new SecurityOptions(routeIpAllowedList, routeIpBlockedList);\r\n\r\n        // Act\r\n        var actual = _creator.Create(securityOptions, globalConfig);\r\n\r\n        // Assert\r\n        actual.ThenTheResultIs(expected);\r\n    }\r\n}\r\n\r\ninternal static class SecurityOptionsExtensions\r\n{\r\n    public static void ThenTheResultIs(this SecurityOptions actual, SecurityOptions expected)\r\n    {\r\n        for (var i = 0; i < expected.IPAllowedList.Count; i++)\r\n        {\r\n            actual.IPAllowedList[i].ShouldBe(expected.IPAllowedList[i]);\r\n        }\r\n\r\n        for (var i = 0; i < expected.IPBlockedList.Count; i++)\r\n        {\r\n            actual.IPBlockedList[i].ShouldBe(expected.IPBlockedList[i]);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/ServiceProviderConfigurationCreatorTests.cs",
    "content": "using Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class ServiceProviderConfigurationCreatorTests : UnitTest\n{\n    private readonly ServiceProviderConfigurationCreator _creator = new();\n\n    [Fact]\n    public void Should_create_service_provider_config()\n    {\n        // Arrange\n        var globalConfig = new FileGlobalConfiguration\n        {\n            ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\n            {\n                Scheme = \"https\",\n                Host = \"127.0.0.1\",\n                Port = 1234,\n                Type = \"ServiceFabric\",\n                Token = \"testtoken\",\n                ConfigurationKey = \"woo\",\n                Namespace = \"default\",\n            },\n        };\n        var expected = new ServiceProviderConfigurationBuilder()\n            .WithScheme(\"https\")\n            .WithHost(\"127.0.0.1\")\n            .WithPort(1234)\n            .WithType(\"ServiceFabric\")\n            .WithToken(\"testtoken\")\n            .WithConfigurationKey(\"woo\")\n            .WithNamespace(\"default\")\n            .Build();\n\n        // Act\n        var result = _creator.Create(globalConfig);\n\n        // Assert\n        result.Scheme.ShouldBe(expected.Scheme);\n        result.Host.ShouldBe(expected.Host);\n        result.Port.ShouldBe(expected.Port);\n        result.Token.ShouldBe(expected.Token);\n        result.Type.ShouldBe(expected.Type);\n        result.Namespace.ShouldBe(expected.Namespace);\n        result.ConfigurationKey.ShouldBe(expected.ConfigurationKey);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/StaticRoutesCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class StaticRoutesCreatorTests : UnitTest\n{\n    private readonly StaticRoutesCreator _creator;\n    private readonly Mock<IClaimsToThingCreator> _cthCreator;\n    private readonly Mock<IAuthenticationOptionsCreator> _aoCreator;\n    private readonly Mock<IUpstreamTemplatePatternCreator> _utpCreator;\n    private readonly Mock<IUpstreamHeaderTemplatePatternCreator> _uhtpCreator;\n    private readonly Mock<IRequestIdKeyCreator> _ridkCreator;\n    private readonly Mock<IQoSOptionsCreator> _qosoCreator;\n    private readonly Mock<IRateLimitOptionsCreator> _rloCreator;\n    private readonly Mock<ICacheOptionsCreator> _coCreator;\n    private readonly Mock<IHttpHandlerOptionsCreator> _hhoCreator;\n    private readonly Mock<IHeaderFindAndReplaceCreator> _hfarCreator;\n    private readonly Mock<IDownstreamAddressesCreator> _daCreator;\n    private readonly Mock<ILoadBalancerOptionsCreator> _lboCreator;\n    private readonly Mock<IRouteKeyCreator> _rrkCreator;\n    private readonly Mock<ISecurityOptionsCreator> _soCreator;\n    private readonly Mock<IVersionCreator> _versionCreator;\n    private readonly Mock<IVersionPolicyCreator> _versionPolicyCreator;\n    private readonly Mock<IMetadataCreator> _metadataCreator;\n    private FileConfiguration _fileConfig;\n    private string _requestId;\n    private string _rrk;\n    private UpstreamPathTemplate _upt;\n    private AuthenticationOptions _ao;\n    private List<ClaimToThing> _ctt;\n    private QoSOptions _qoso;\n    private RateLimitOptions _rlo;\n    private CacheOptions _cacheOptions;\n    private HttpHandlerOptions _hho;\n    private HeaderTransformations _ht;\n    private List<DownstreamHostAndPort> _dhp;\n    private LoadBalancerOptions _lbo;\n    private IReadOnlyList<Route> _result;\n    private Version _expectedVersion;\n    private HttpVersionPolicy _expectedVersionPolicy;\n    private Dictionary<string, UpstreamHeaderTemplate> _uht;\n    private Dictionary<string, string> _expectedMetadata;\n\n    public StaticRoutesCreatorTests()\n    {\n        _cthCreator = new Mock<IClaimsToThingCreator>();\n        _aoCreator = new Mock<IAuthenticationOptionsCreator>();\n        _utpCreator = new Mock<IUpstreamTemplatePatternCreator>();\n        _ridkCreator = new Mock<IRequestIdKeyCreator>();\n        _qosoCreator = new Mock<IQoSOptionsCreator>();\n        _rloCreator = new Mock<IRateLimitOptionsCreator>();\n        _coCreator = new Mock<ICacheOptionsCreator>();\n        _hhoCreator = new Mock<IHttpHandlerOptionsCreator>();\n        _hfarCreator = new Mock<IHeaderFindAndReplaceCreator>();\n        _daCreator = new Mock<IDownstreamAddressesCreator>();\n        _lboCreator = new Mock<ILoadBalancerOptionsCreator>();\n        _rrkCreator = new Mock<IRouteKeyCreator>();\n        _soCreator = new Mock<ISecurityOptionsCreator>();\n        _versionCreator = new Mock<IVersionCreator>();\n        _versionPolicyCreator = new Mock<IVersionPolicyCreator>();\n        _uhtpCreator = new Mock<IUpstreamHeaderTemplatePatternCreator>();\n        _metadataCreator = new Mock<IMetadataCreator>();\n\n        _creator = new StaticRoutesCreator(\n            _cthCreator.Object,\n            _aoCreator.Object,\n            _utpCreator.Object,\n            _ridkCreator.Object,\n            _qosoCreator.Object,\n            _rloCreator.Object,\n            _coCreator.Object,\n            _hhoCreator.Object,\n            _hfarCreator.Object,\n            _daCreator.Object,\n            _lboCreator.Object,\n            _rrkCreator.Object,\n            _soCreator.Object,\n            _versionCreator.Object,\n            _versionPolicyCreator.Object,\n            _uhtpCreator.Object,\n            _metadataCreator.Object);\n    }\n\n    [Fact]\n    public void Should_return_nothing()\n    {\n        // Arrange\n        _fileConfig = new FileConfiguration();\n\n        // Act\n        _result = _creator.Create(_fileConfig);\n\n        // Assert\n        _result.ShouldBeEmpty();\n    }\n\n    [Fact]\n    public void Should_return_routes()\n    {\n        // Arrange\n        _fileConfig = new FileConfiguration\n        {\n            Routes = new List<FileRoute>\n            {\n                new()\n                {\n                    ServiceName = \"dave\",\n                    DangerousAcceptAnyServerCertificateValidator = true,\n                    AddClaimsToRequest = new Dictionary<string, string>\n                    {\n                        { \"a\",\"b\" },\n                    },\n                    AddHeadersToRequest = new Dictionary<string, string>\n                    {\n                        { \"c\",\"d\" },\n                    },\n                    AddQueriesToRequest = new Dictionary<string, string>\n                    {\n                        { \"e\",\"f\" },\n                    },\n                    UpstreamHttpMethod = [\"GET\", \"POST\"],\n                    Metadata = new Dictionary<string, string>\n                    {\n                        [\"foo\"] = \"bar\",\n                    },\n                    LoadBalancerOptions = new(\"LB1\"),\n                    CacheOptions = new() { Region = \"dave\" },\n                },\n                new()\n                {\n                    ServiceName = \"wave\",\n                    DangerousAcceptAnyServerCertificateValidator = false,\n                    AddClaimsToRequest = new Dictionary<string, string>\n                    {\n                        { \"g\",\"h\" },\n                    },\n                    AddHeadersToRequest = new Dictionary<string, string>\n                    {\n                        { \"i\",\"j\" },\n                    },\n                    AddQueriesToRequest = new Dictionary<string, string>\n                    {\n                        { \"k\",\"l\" },\n                    },\n                    UpstreamHttpMethod = [\"PUT\", \"DELETE\"],\n                    Metadata = new Dictionary<string, string>\n                    {\n                        [\"foo\"] = \"baz\",\n                    },\n                    LoadBalancerOptions = new(\"LB2\"),\n                    CacheOptions = new() { Region = \"wave\" },\n                },\n            },\n        };\n        GivenTheDependenciesAreSetUpCorrectly();\n\n        // Act\n        _result = _creator.Create(_fileConfig);\n\n        // Assert\n        ThenTheDependenciesAreCalledCorrectly();\n        ThenTheRoutesAreCreated();\n    }\n\n    #region PR 2073\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")] // https://github.com/ThreeMammals/Ocelot/pull/2073\n    [Trait(\"Feat\", \"1314\")] // https://github.com/ThreeMammals/Ocelot/issues/1314\n    [Trait(\"Feat\", \"1869\")] // https://github.com/ThreeMammals/Ocelot/issues/1869\n    public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute()\n    {\n        // Arrange\n        var route = new FileRoute { Timeout = 11 };\n        var global = new FileGlobalConfiguration { Timeout = 22 };\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Equal(route.Timeout, timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig()\n    {\n        // Arrange\n        var route = new FileRoute();\n        var global = new FileGlobalConfiguration { Timeout = 22 };\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Null(route.Timeout);\n        Assert.Equal(global.Timeout, timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults()\n    {\n        // Arrange\n        var route = new FileRoute();\n        var global = new FileGlobalConfiguration();\n\n        // Act\n        var timeout = _creator.CreateTimeout(route, global);\n\n        // Assert\n        Assert.Null(route.Timeout);\n        Assert.Null(global.Timeout);\n        Assert.Equal(DownstreamRoute.DefTimeout, timeout);\n    }\n    #endregion\n\n    [Theory]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(\"\", 0)]\n    [InlineData(\"A\", 1)]\n    [InlineData(\"A,B\", 2)]\n    [InlineData(\" X \", 1)]\n    public void SetUpRoute_FileRouteUpstreamHttpMethod(string methods, int count)\n    {\n        // Arrange\n        _fileConfig = new FileConfiguration();\n        _fileConfig.Routes.Add(new() { UpstreamHttpMethod = methods.Split(',').Where(m => !string.IsNullOrEmpty(m)).ToHashSet() });\n        GivenTheDependenciesAreSetUpCorrectly();\n\n        // Act\n        _result = _creator.Create(_fileConfig);\n\n        // Assert\n        Assert.Equal(count, _result[0].UpstreamHttpMethod.Count);\n    }\n\n    private void ThenTheDependenciesAreCalledCorrectly()\n    {\n        ThenTheDepsAreCalledFor(_fileConfig.Routes[0], _fileConfig.GlobalConfiguration);\n        ThenTheDepsAreCalledFor(_fileConfig.Routes[1], _fileConfig.GlobalConfiguration);\n    }\n\n    private void GivenTheDependenciesAreSetUpCorrectly()\n    {\n        _expectedVersion = new Version(\"1.1\");\n        _expectedVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;\n        _requestId = \"testy\";\n        _rrk = \"besty\";\n        _upt = new UpstreamPathTemplateBuilder().Build();\n        _ao = new AuthenticationOptions();\n        _ctt = new List<ClaimToThing>();\n        _qoso = new QoSOptions();\n        _rlo = new RateLimitOptions();\n\n        _cacheOptions = new CacheOptions(0, \"vesty\", null, false);\n        _hho = new();\n        _ht = new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>(), new List<AddHeader>(), new List<AddHeader>());\n        _dhp = new List<DownstreamHostAndPort>();\n        _lbo = new();\n        _uht = new Dictionary<string, UpstreamHeaderTemplate>();\n        _expectedMetadata = new Dictionary<string, string>()\n        {\n            [\"foo\"] = \"bar\",\n        };\n\n        _ridkCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_requestId);\n        _rrkCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<LoadBalancerOptions>())).Returns(_rrk);\n        _utpCreator.Setup(x => x.Create(It.IsAny<IRouteUpstream>())).Returns(_upt);\n        _aoCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_ao);\n        _cthCreator.Setup(x => x.Create(It.IsAny<Dictionary<string, string>>())).Returns(_ctt);\n        _qosoCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_qoso);\n        _rloCreator.Setup(x => x.Create(It.IsAny<IRouteRateLimiting>(), It.IsAny<FileGlobalConfiguration>())).Returns(_rlo);\n        _coCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>(), It.IsAny<string>())).Returns(_cacheOptions);\n        _hhoCreator.Setup(x => x.Create(It.IsAny<FileHttpHandlerOptions>())).Returns(_hho);\n        _hhoCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_hho);\n        _hfarCreator.Setup(x => x.Create(It.IsAny<FileRoute>())).Returns(_ht);\n        _hfarCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_ht);\n        _daCreator.Setup(x => x.Create(It.IsAny<FileRoute>())).Returns(_dhp);\n        _lboCreator.Setup(x => x.Create(It.IsAny<FileRoute>(), It.IsAny<FileGlobalConfiguration>())).Returns(_lbo);\n        _versionCreator.Setup(x => x.Create(It.IsAny<string>())).Returns(_expectedVersion);\n        _versionPolicyCreator.Setup(x => x.Create(It.IsAny<string>())).Returns(_expectedVersionPolicy);\n        _uhtpCreator.Setup(x => x.Create(It.IsAny<FileRoute>())).Returns(_uht);\n        _metadataCreator.Setup(x => x.Create(It.IsAny<IDictionary<string, string>>(), It.IsAny<FileGlobalConfiguration>()))\n            .Returns(new MetadataOptions() { Metadata = _expectedMetadata });\n    }\n\n    private void ThenTheRoutesAreCreated()\n    {\n        _result.Count.ShouldBe(2);\n        ThenTheRouteIsSet(_fileConfig.Routes[0], 0);\n        ThenTheRouteIsSet(_fileConfig.Routes[1], 1);\n    }\n\n    private void ThenTheRouteIsSet(FileRoute expected, int routeIndex)\n    {\n        _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion);\n        _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_expectedVersionPolicy);\n        _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBeFalse();\n        _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBeFalse();\n        _result[routeIndex].DownstreamRoute[0].CacheOptions.UseCache.ShouldBeFalse();\n        _result[routeIndex].DownstreamRoute[0].RequestIdKey.ShouldBe(_requestId);\n        _result[routeIndex].DownstreamRoute[0].LoadBalancerKey.ShouldBe(_rrk);\n        _result[routeIndex].DownstreamRoute[0].UpstreamPathTemplate.ShouldBe(_upt);\n        _result[routeIndex].DownstreamRoute[0].AuthenticationOptions.ShouldBe(_ao);\n        _result[routeIndex].DownstreamRoute[0].ClaimsToHeaders.ShouldBe(_ctt);\n        _result[routeIndex].DownstreamRoute[0].ClaimsToQueries.ShouldBe(_ctt);\n        _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt);\n        _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso);\n        _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo);\n        _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region);\n        _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0);\n        _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho);\n        _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream);\n        _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream);\n        _result[routeIndex].DownstreamRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream);\n        _result[routeIndex].DownstreamRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream);\n        _result[routeIndex].DownstreamRoute[0].DownstreamAddresses.ShouldBe(_dhp);\n        _result[routeIndex].DownstreamRoute[0].LoadBalancerOptions.ShouldBe(_lbo);\n        _result[routeIndex].DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue();\n        _result[routeIndex].DownstreamRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator);\n        _result[routeIndex].DownstreamRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers);\n        _result[routeIndex].DownstreamRoute[0].ServiceName.ShouldBe(expected.ServiceName);\n        _result[routeIndex].DownstreamRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme);\n        _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement);\n        _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate);\n        _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key);\n        _result[routeIndex].DownstreamRoute[0].MetadataOptions.Metadata.ShouldBe(_expectedMetadata);\n        _result[routeIndex].UpstreamHttpMethod.Count.ShouldBe(2);\n        _result[routeIndex].UpstreamHttpMethod.ShouldAllBe(actual => expected.UpstreamHttpMethod.Contains(actual.Method));\n        _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost);\n        _result[routeIndex].DownstreamRoute.Count.ShouldBe(1);\n        _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt);\n        _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht);\n    }\n\n    private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig)\n    {\n        _ridkCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _rrkCreator.Verify(x => x.Create(fileRoute, It.IsAny<LoadBalancerOptions>()), Times.Once);\n        _utpCreator.Verify(x => x.Create(fileRoute), Times.Exactly(2));\n        _aoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _cthCreator.Verify(x => x.Create(fileRoute.AddHeadersToRequest), Times.Once);\n        _cthCreator.Verify(x => x.Create(fileRoute.AddClaimsToRequest), Times.Once);\n        _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once);\n        _qosoCreator.Verify(x => x.Create(fileRoute, globalConfig));\n        _rloCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _coCreator.Verify(x => x.Create(fileRoute, globalConfig, _rrk), Times.Once);\n        _hhoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _hfarCreator.Verify(x => x.Create(fileRoute), Times.Never);\n        _hfarCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _daCreator.Verify(x => x.Create(fileRoute), Times.Once);\n        _lboCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once);\n        _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions, globalConfig), Times.Once);\n        _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class UpstreamHeaderTemplatePatternCreatorTests\n{\n    private readonly UpstreamHeaderTemplatePatternCreator _creator = new();\n\n    [Trait(\"PR\", \"1312\")]\n    [Trait(\"Feat\", \"360\")]\n    [Theory(DisplayName = \"Should create pattern\")]\n    [InlineData(\"country\", \"a text without placeholders\", \"^(?i)a text without placeholders$\", \" without placeholders\")]\n    [InlineData(\"country\", \"a text without placeholders\", \"^a text without placeholders$\", \" Route is case sensitive\", true)]\n    [InlineData(\"country\", \"{header:start}rest of the text\", \"^(?i)(?<start>.+)rest of the text$\", \" with placeholder in the beginning\")]\n    [InlineData(\"country\", \"rest of the text{header:end}\", \"^(?i)rest of the text(?<end>.+)$\", \" with placeholder at the end\")]\n    [InlineData(\"country\", \"{header:countrycode}\", \"^(?i)(?<countrycode>.+)$\", \" with placeholder only\")]\n    [InlineData(\"country\", \"any text {header:cc} and other {header:version} and {header:bob} the end\", \"^(?i)any text (?<cc>.+) and other (?<version>.+) and (?<bob>.+) the end$\", \" with more placeholders\")]\n    public void Create_WithUpstreamHeaderTemplates_ShouldCreatePattern(string key, string template, string expected, string withMessage, bool? isCaseSensitive = null)\n    {\n        // Arrange\n        var fileRoute = new FileRoute\n        {\n            RouteIsCaseSensitive = isCaseSensitive ?? false,\n            UpstreamHeaderTemplates = new Dictionary<string, string>\n            {\n                [key] = template,\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(fileRoute);\n\n        // Assert\n        var message = nameof(Create_WithUpstreamHeaderTemplates_ShouldCreatePattern).Replace('_', ' ') + withMessage;\n        actual[key].ShouldNotBeNull()\n            .Template.ShouldBe(expected, message);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs",
    "content": "using Ocelot.Cache;\r\nusing Ocelot.Configuration.Creator;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Values;\r\nusing System;\r\nusing System.Reflection;\r\nusing System.Text.RegularExpressions;\r\n\r\nnamespace Ocelot.UnitTests.Configuration;\r\n\r\npublic class UpstreamTemplatePatternCreatorTests : UnitTest\r\n{\r\n    private readonly Mock<IOcelotCache<Regex>> _cache;\r\n    private readonly UpstreamTemplatePatternCreator _creator;\r\n    private const string MatchEverything = UpstreamTemplatePatternCreator.RegExMatchZeroOrMoreOfEverything;\r\n\r\n    public UpstreamTemplatePatternCreatorTests()\r\n    {\r\n        _cache = new();\r\n        _creator = new UpstreamTemplatePatternCreator(_cache.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_match_up_to_next_slash()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/v{apiVersion}/cards\",\r\n            Priority = 0,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^(?i)/api/v[^/]+/cards$\");\r\n        result.Priority.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_re_route_priority()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/orders/{catchAll}\",\r\n            Priority = 0,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^(?i)/orders(?:|/{MatchEverything})$\");\r\n        result.Priority.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_zero_priority()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/{catchAll}\",\r\n            Priority = 1,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^/.*\");\r\n        result.Priority.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_set_upstream_template_pattern_to_ignore_case_sensitivity()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/PRODUCTS/{productId}\",\r\n            RouteIsCaseSensitive = false,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^(?i)/PRODUCTS(?:|/{MatchEverything})$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/PRODUCTS/\",\r\n            RouteIsCaseSensitive = false,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^(?i)/PRODUCTS(/|)$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_set_upstream_template_pattern_to_respect_case_sensitivity()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/PRODUCTS/{productId}\",\r\n            RouteIsCaseSensitive = true,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^/PRODUCTS(?:|/{MatchEverything})$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_anything_to_end_of_string()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/products/{productId}\",\r\n            RouteIsCaseSensitive = true,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^/api/products(?:|/{MatchEverything})$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_more_than_one_placeholder()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/products/{productId}/variants/{variantId}\",\r\n            RouteIsCaseSensitive = true,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^/api/products/[^/]+/variants(?:|/{MatchEverything})$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/products/{productId}/variants/{variantId}/\",\r\n            RouteIsCaseSensitive = true,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^/api/products/[^/]+/variants/[^/]+(/|)$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_to_end_of_string()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^/$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/{url}\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^/.*\");\r\n        result.Priority.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_starts_with_placeholder_then_has_another_later()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/{productId}/products/variants/{variantId}/\",\r\n            RouteIsCaseSensitive = true,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe(\"^/[^/]+/products/variants/[^/]+(/|)$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_query_string()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($@\"^(?i)/api/subscriptions/[^/]+/updates(/$|/\\?|\\?|$)unitId={MatchEverything}$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_create_template_pattern_that_matches_query_string_with_multiple_params()\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = \"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}\",\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.Template.ShouldBe($\"^(?i)/api/subscriptions/[^/]+/updates(/$|/\\\\?|\\\\?|$)unitId={MatchEverything}&productId={MatchEverything}$\");\r\n        result.Priority.ShouldBe(1);\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"2064\")]\r\n    [InlineData(\"/{tenantId}/products?{everything}\", \"/1/products/1\", false)]\r\n    [InlineData(\"/{tenantId}/products/{everything}\", \"/1/products/1\", true)]\r\n    public void Should_not_match_when_placeholder_appears_after_query_start(string urlPathTemplate, string requestPath, bool shouldMatch)\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = urlPathTemplate,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.ShouldMatchWithRegex(requestPath, shouldMatch);\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Bug\", \"2132\")]\r\n    [InlineData(\"/api/v1/abc?{everything}\", \"/api/v1/abc2/apple\", false)]\r\n    [InlineData(\"/api/v1/abc2/{everything}\", \"/api/v1/abc2/apple\", true)]\r\n    public void Should_not_match_with_query_param_wildcard(string urlPathTemplate, string requestPath, bool shouldMatch)\r\n    {\r\n        // Arrange\r\n        var fileRoute = new FileRoute\r\n        {\r\n            UpstreamPathTemplate = urlPathTemplate,\r\n        };\r\n\r\n        // Act\r\n        var result = _creator.Create(fileRoute);\r\n\r\n        // Assert\r\n        result.ShouldMatchWithRegex(requestPath, shouldMatch);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"1348\")]\r\n    [Trait(\"Bug\", \"2246\")]\r\n    public void GetRegex_NoKey_ReturnsNull()\r\n    {\r\n        // Act\r\n        var actual = GetRegex.Invoke(_creator, new object[] { string.Empty });\r\n\r\n        // Assert\r\n        actual.ShouldBeNull();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"1348\")]\r\n    [Trait(\"Bug\", \"2246\")]\r\n    public void CreateTemplate_PatternProperty_NullChecks()\r\n    {\r\n        // Act\r\n        string nullTemplate = null;\r\n        var actual = CreateTemplate.Invoke(_creator, new object[] { nullTemplate, 0, false, null }) as UpstreamPathTemplate;\r\n\r\n        // Assert\r\n        actual.ShouldNotBeNull();\r\n        actual.Pattern.ShouldNotBeNull().ToString().ShouldBe(\"$^\");\r\n    }\r\n\r\n    private static Type Me { get; } = typeof(UpstreamTemplatePatternCreator);\r\n    private static MethodInfo GetRegex { get; } = Me.GetMethod(nameof(GetRegex), BindingFlags.NonPublic | BindingFlags.Instance);\r\n    private static MethodInfo CreateTemplate { get; } = Me.GetMethod(nameof(CreateTemplate), BindingFlags.NonPublic | BindingFlags.Instance);\r\n}\r\n\r\ninternal static class UpstreamPathTemplateExtensions\r\n{\r\n    public static void ShouldMatchWithRegex(this UpstreamPathTemplate actual, string requestPath, bool shouldMatch)\r\n    {\r\n        var match = Regex.Match(requestPath, actual.Template);\r\n        Assert.Equal(shouldMatch, match.Success);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Validation/FileAuthenticationOptionsValidatorTests.cs",
    "content": "﻿using FluentValidation.Results;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\n\nnamespace Ocelot.UnitTests.Configuration.Validation;\n\n[Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\n[Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\npublic class FileAuthenticationOptionsValidatorTests : UnitTest\n{\n    private readonly FileAuthenticationOptionsValidator _validator;\n    private readonly Mock<IAuthenticationSchemeProvider> _authProvider;\n    private FileAuthenticationOptions _authenticationOptions;\n    private ValidationResult _result;\n\n    public FileAuthenticationOptionsValidatorTests()\n    {\n        _authProvider = new();\n        _validator = new(_authProvider.Object);\n    }\n\n    [Fact]\n    public async Task Should_be_valid_if_specified_authentication_provider_is_registered()\n    {\n        // Arrange\n        const string key = \"JwtLads\";\n        CreateAuthenticationOptions(key);\n        GivenAnAuthProvider(key);\n\n        // Act\n        await ValidateAsync();\n\n        // Assert\n        _result.IsValid.ShouldBeTrue();\n    }\n\n    [Fact]\n    public async Task Should_not_be_valid_if_specified_authentication_provider_is_not_registered()\n    {\n        // Arrange\n        const string key = \"JwtLads\";\n        CreateAuthenticationOptions(key);\n\n        // Act\n        await ValidateAsync();\n\n        // Assert\n        _result.IsValid.ShouldBeFalse();\n        _result.Errors[0].ErrorMessage.ShouldBe(\"AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'',AuthenticationProviderKeys:['JwtLads'] is unsupported authentication provider\");\n    }\n\n    private void GivenAnAuthProvider(string key)\n    {\n        var schemes = new List<AuthenticationScheme>\n        {\n            new(key, key, typeof(FakeAuthHandler)),\n        };\n        _authProvider.Setup(x => x.GetAllSchemesAsync())\n            .ReturnsAsync(schemes);\n    }\n\n    private void CreateAuthenticationOptions(string key)\n    {\n        _authenticationOptions = new FileAuthenticationOptions\n        {\n            AuthenticationProviderKeys = [ key ],\n        };\n    }\n\n    private async Task ValidateAsync()\n    {\n        _result = await _validator.ValidateAsync(_authenticationOptions);\n    }\n\n    private class FakeAuthHandler : IAuthenticationHandler\n    {\n        public Task<AuthenticateResult> AuthenticateAsync() => throw new NotImplementedException();\n        public Task ChallengeAsync(AuthenticationProperties properties) => throw new NotImplementedException();\n        public Task ForbidAsync(AuthenticationProperties properties) => throw new NotImplementedException();\n        public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\r\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\nusing Ocelot.Logging;\r\nusing Ocelot.QualityOfService;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.UnitTests.Requester;\nusing Ocelot.Values;\nusing System.Security.Claims;\nusing System.Text.Encodings.Web;\r\n\r\nnamespace Ocelot.UnitTests.Configuration.Validation;\r\n\r\npublic class FileConfigurationFluentValidatorTests : UnitTest\r\n{\r\n    private FileConfigurationFluentValidator _configurationValidator;\r\n    private FileConfiguration _fileConfiguration;\r\n    private Response<ConfigurationValidationResult> _result;\r\n    private IServiceProvider _provider;\n    private readonly ServiceCollection _services;\r\n    private readonly Mock<IAuthenticationSchemeProvider> _authProvider;\r\n    private readonly FileAuthenticationOptionsValidator _fileAuthOptsValidator;\r\n\r\n    public FileConfigurationFluentValidatorTests()\r\n    {\r\n        _services = new ServiceCollection();\r\n        _authProvider = new Mock<IAuthenticationSchemeProvider>();\r\n        _fileAuthOptsValidator = new FileAuthenticationOptionsValidator(_authProvider.Object);\r\n        _provider = _services.BuildServiceProvider(true);\r\n\r\n        // TODO Replace with mocks\r\n        _configurationValidator = new FileConfigurationFluentValidator(\r\n            _provider,\r\n            new(new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_provider), _fileAuthOptsValidator),\r\n            new(new FileQoSOptionsFluentValidator(_provider), _fileAuthOptsValidator));\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option()\r\n    {\r\n        // Arrange\r\n        var route = GivenServiceDiscoveryRoute();\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenServiceDiscoveryRoute();\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = \"FakeServiceDiscoveryProvider\";\r\n        GivenAConfiguration(configuration);\r\n        GivenAServiceDiscoveryHandler();\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler()\r\n    {\r\n        // Arrange\r\n        var configuration = new FileConfiguration();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = \"FakeServiceDiscoveryProvider\";\r\n        GivenAConfiguration(configuration);\r\n        GivenAServiceDiscoveryHandler();\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenServiceDiscoveryRoute();\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = \"FakeServiceDiscoveryProvider\";\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler()\r\n    {\r\n        // Arrange\r\n        var configuration = new FileConfiguration();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = \"FakeServiceDiscoveryProvider\";\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name()\r\n    {\r\n        // Arrange\r\n        var route = GivenServiceDiscoveryRoute();\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\n        configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = \"consul\";\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n        GivenAServiceDiscoveryHandler();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_qos_options_specified_and_has_qos_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\r\n        route.QoSOptions = new FileQoSOptions\n        {\r\n            TimeoutValue = 1,\n            ExceptionsAllowedBeforeBreaking = 1,\n        };\r\n        GivenAConfiguration(route);\r\n        GivenAQoSHandler();\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.QoSOptions = new()\n        {\r\n            TimeoutValue = 1,\n            ExceptionsAllowedBeforeBreaking = 1,\n        };\r\n        GivenAConfiguration(configuration);\r\n        GivenAQoSHandler();\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_qos_options_specified_but_no_qos_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        route.QoSOptions = new FileQoSOptions\n        {\r\n            TimeoutValue = 1,\n            ExceptionsAllowedBeforeBreaking = 1,\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var configuration = GivenAConfiguration(route);\n        configuration.GlobalConfiguration.QoSOptions = new()\n        {\r\n            TimeoutValue = 1,\n            ExceptionsAllowedBeforeBreaking = 1,\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_aggregates_are_valid()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var route2 = GivenDefaultRoute(\"/tom\", \"/\", key: \"Tom\");\n        var configuration = GivenAConfiguration(route, route2);\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_aggregates_are_duplicate_of_routes()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var route2 = GivenDefaultRoute(\"/tom\", \"/\", key: \"Tom\", upstreamHost: \"localhost\");\r\n        var configuration = GivenAConfiguration(route, route2);\r\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/tom\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"route /tom has duplicate aggregate\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_if_aggregates_are_not_duplicate_of_routes()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var route2 = GivenDefaultRoute(\"/tom\", \"/\", key: \"Tom\");\r\n        route2.UpstreamHttpMethod.Add(\"Post\");\n        var configuration = GivenAConfiguration(route, route2);\r\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/tom\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var route2 = GivenDefaultRoute(\"/lol\", \"/\", key: \"Tom\");\n        var configuration = GivenAConfiguration(route, route2);\r\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/tom\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/tom\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"aggregate /tom has duplicate aggregate\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_routes_dont_exist_for_aggregate()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var configuration = GivenAConfiguration(route);\r\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Routes for aggregateRoute / either do not exist or do not have correct ServiceName property\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_aggregate_has_routes_with_specific_request_id_keys()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(\"/laura\", \"/\", key: \"Laura\");\n        var route2 = GivenDefaultRoute(\"/tom\", \"/\", key: \"Tom\");\n        route2.RequestIdKey = \"should_fail\";\n        var configuration = GivenAConfiguration(route, route2);\r\n        configuration.Aggregates = new()\n        {\r\n            new()\r\n            {\r\n                UpstreamPathTemplate = \"/\",\r\n                UpstreamHost = \"localhost\",\r\n                RouteKeys = [\"Tom\", \"Laura\"],\r\n            },\r\n        };\r\n        GivenAConfiguration(configuration);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"aggregateRoute / contains Route with specific RequestIdKey, this is not possible with Aggregates\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_scheme_in_downstream_or_upstream_template()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute(\"http://asdf.com\", \"http://www.bbc.co.uk/api/products/{productId}\"));\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorIs<FileValidationFailedError>();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash\");\r\n        ThenTheErrorMessageAtPositionIs(1, \"Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n        ThenTheErrorMessageAtPositionIs(2, \"Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme\");\r\n\r\n        ThenTheErrorMessageAtPositionIs(3, \"Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n        ThenTheErrorMessageAtPositionIs(4, \"Upstream Path Template http://asdf.com doesnt start with forward slash\");\r\n        ThenTheErrorMessageAtPositionIs(5, \"Upstream Path Template http://asdf.com contains scheme\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_one_route()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute());\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_without_slash_prefix_downstream_path_template()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute(\"/asdf/\", \"api/products/\"));\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Downstream Path Template api/products/ doesnt start with forward slash\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_without_slash_prefix_upstream_path_template()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute(\"api/prod/\", \"/api/products/\"));\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Upstream Path Template api/prod/ doesnt start with forward slash\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute(\"//api/prod/\", \"/api/products/\"));\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Upstream Path Template //api/prod/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash()\r\n    {\r\n        // Arrange\r\n        GivenAConfiguration(GivenDefaultRoute(\"/api/prod/\", \"//api/products/\"));\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"Downstream Path Template //api/products/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_valid_authentication_provider()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        route.AuthenticationOptions = new() { AuthenticationProviderKey = \"Test\" };\r\n        GivenAConfiguration(route);\r\n        GivenTheAuthSchemeExists(\"Test\");\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_invalid_with_invalid_authentication_provider()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        route.AuthenticationOptions = new FileAuthenticationOptions()\n        {\n            AuthenticationProviderKey = \"Test\",\n            AuthenticationProviderKeys = new string[] { \"Test #1\", \"Test #2\" },\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'Test',AuthenticationProviderKeys:['Test #1','Test #2'] is unsupported authentication provider\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_not_valid_with_duplicate_routes_all_verbs()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\");\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"route /asdf/ has duplicate\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_duplicate_routes_all_verbs_but_different_hosts()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(null, null, upstreamHost: \"host1\");\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\", upstreamHost: \"host2\");\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_not_valid_with_duplicate_routes_specific_verbs()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\");\n        duplicate.UpstreamHttpMethod = [HttpMethods.Get];\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"route /asdf/ has duplicate\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_duplicate_routes_different_verbs()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(); // \"Get\" verb is inside\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\");\n        duplicate.UpstreamHttpMethod = [HttpMethods.Post];\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: \"upstreamhost\");\r\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\", methods: [], upstreamHost: \"upstreamhost\");\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"route /asdf/ has duplicate\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: \"upstreamhost111\");\r\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\", methods: [], upstreamHost: \"upstreamhost222\");\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: \"upstreamhost\");\r\n        var duplicate = GivenDefaultRoute(null, \"/www/test/\", methods: [], upstreamHost: null); // !\r\n        GivenAConfiguration(route, duplicate);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\n    [Fact]\r\n    public async Task Configuration_is_invalid_with_invalid_rate_limit_configuration()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        route.RateLimitOptions = new FileRateLimitByHeaderRule\n        {\n            Period = \"1x\",\n            EnableRateLimiting = true,\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_valid_rate_limit_configuration()\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        route.RateLimitOptions = new FileRateLimitByHeaderRule\n        {\n            Period = \"1d\",\n            EnableRateLimiting = true,\r\n        };\n        GivenAConfiguration(route);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_with_using_service_discovery_and_service_name()\r\n    {\r\n        // Arrange\r\n        var route = GivenServiceDiscoveryRoute();\n        var config = GivenAConfiguration(route);\n        config.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider();\r\n        GivenAConfiguration(config);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    private const string Empty = \"\";\n\r\n    [Theory]\r\n    [InlineData(null)]\r\n    [InlineData(Empty)]\r\n    public async Task Configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost)\r\n    {\r\n        // Arrange\r\n        var route = GivenDefaultRoute();\n        route.DownstreamHostAndPorts[0].Host = downstreamHost;\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\r\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!\");\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(null, true)]\r\n    [InlineData(Empty, true)]\r\n    [InlineData(\"Test\", false)]\r\n    public async Task HaveServiceDiscoveryProviderRegistered_RouteServiceName_Validated(string serviceName, bool valid)\n    {\n        // Arrange\n        var route = GivenDefaultRoute();\n        route.ServiceName = serviceName;\r\n        var config = GivenAConfiguration(route);\n        config.GlobalConfiguration.ServiceDiscoveryProvider = null;\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        _result.Data.IsError.ShouldNotBe(valid);\n        _result.Data.Errors.Count.ShouldBe(valid ? 0 : 1);\n    }\r\n\r\n    [Theory]\r\n    [InlineData(false, null, false)]\r\n    [InlineData(true, null, false)]\r\n    [InlineData(true, \"type\", false)]\r\n    [InlineData(true, \"servicefabric\", true)]\r\n    public async Task HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryProvider_Validated(bool create, string type, bool valid)\n    {\r\n        // Arrange\n        var route = GivenServiceDiscoveryRoute();\n        var config = GivenAConfiguration(route);\n        var provider = create ? GivenDefaultServiceDiscoveryProvider() : null;\n        config.GlobalConfiguration.ServiceDiscoveryProvider = provider;\n        if (create && provider != null)\n        {\r\n            provider.Type = type;\n        }\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        _result.Data.IsError.ShouldNotBe(valid);\n        _result.Data.Errors.Count.ShouldBeGreaterThanOrEqualTo(valid ? 0 : 1);\n    }\n\n    [Theory]\n    [InlineData(false)]\r\n    [InlineData(true)]\r\n    public async Task HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryFinderDelegates_Validated(bool hasDelegate)\n    {\n        // Arrange\n        var valid = hasDelegate;\n        var route = GivenServiceDiscoveryRoute();\n        var config = GivenAConfiguration(route);\n        config.GlobalConfiguration.ServiceDiscoveryProvider = null;\n        if (hasDelegate)\n        {\n            GivenAServiceDiscoveryHandler();\n        }\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        _result.Data.IsError.ShouldNotBe(valid);\n        _result.Data.Errors.Count.ShouldBe(valid ? 0 : 1);\n    }\r\n\n    [Fact]\r\n    public async Task Configuration_is_valid_when_not_using_service_discovery_and_host_is_set()\r\n    {\n        // Arrange\n        var route = GivenDefaultRoute();\n        route.DownstreamHostAndPorts = new()\n        {\r\n            new(\"bbc.co.uk\", 123),\r\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_valid_when_no_downstream_but_has_host_and_port()\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute();\n        route.DownstreamHostAndPorts = new()\n        {\r\n            new(\"test\", 123),\r\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_not_valid_when_no_host_and_port()\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute();\n        route.DownstreamHostAndPorts = new();\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Configuration_is_not_valid_when_host_and_port_is_empty()\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute();\n        route.DownstreamHostAndPorts = new()\n        {\r\n            new(),\r\n        };\r\n        GivenAConfiguration(route);\r\n\r\n        // Act\n        await WhenIValidateTheConfiguration();\r\n\r\n        // Assert\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!\");\r\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public async Task Configuration_is_not_valid_when_upstream_headers_the_same()\r\n    {\n        // Arrange\r\n        var route1 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/api/products/\", new()\n                        {\n                            { \"header1\", \"value1\" },\n                            { \"header2\", \"value2\" },\n                        });\n        var route2 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/www/test/\", new()\n                        {\n                            { \"header2\", \"value2\" },\n                            { \"header1\", \"value1\" },\n                        });\n        GivenAConfiguration(route1, route2);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsNotValid();\n        ThenTheErrorMessageAtPositionIs(0, \"route /asdf/ has duplicate\");\r\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public async Task Configuration_is_valid_when_upstream_headers_not_the_same()\r\n    {\r\n        // Arrange\r\n        var route1 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/api/products/\", new()\n                        {\n                            { \"header1\", \"value1\" },\n                            { \"header2\", \"value2\" },\n                        });\n        var route2 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/www/test/\", new()\n                        {\n                            { \"header2\", \"value2\" },\n                            { \"header1\", \"valueDIFFERENT\" },\n                        });\n        GivenAConfiguration(route1, route2);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsValid();\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public async Task Configuration_is_valid_when_upstream_headers_count_not_the_same()\r\n    {\r\n        // Arrange\r\n        var route1 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/api/products/\", new()\n                        {\n                            { \"header1\", \"value1\" },\n                            { \"header2\", \"value2\" },\n                        });\n        var route2 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/www/test/\", new()\n                        {\n                            { \"header2\", \"value2\" },\n                        });\n        GivenAConfiguration(route1, route2);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsValid();\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public async Task Configuration_is_valid_when_one_upstream_headers_empty_and_other_not_empty()\r\n    {\r\n        // Arrange\r\n        var route1 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/api/products/\", new()\n                        {\n                            { \"header1\", \"value1\" },\n                            { \"header2\", \"value2\" },\n                        });\n        var route2 = GivenRouteWithUpstreamHeaderTemplates(\"/asdf/\", \"/www/test/\", new());\n        GivenAConfiguration(route1, route2);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsValid();\n    }\r\n\n    [Theory]\r\n    [Trait(\"PR\", \"1927\")]\n    [InlineData(\"/foo/{bar}/foo\", \"/yahoo/foo/{bar}\")] // valid\r\n    [InlineData(\"/foo/{bar}/{foo}\", \"/yahoo/{foo}/{bar}\")] // valid\r\n    [InlineData(\"/foo/{bar}/{bar}\", \"/yahoo/foo/{bar}\", \"UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder\")] // invalid\r\n    [InlineData(\"/foo/{bar}/{bar}\", \"/yahoo/{foo}/{bar}\", \"UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder\")] // invalid\r\n    [InlineData(\"/yahoo/foo/{bar}\", \"/foo/{bar}/foo\")] // valid\r\n    [InlineData(\"/yahoo/{foo}/{bar}\", \"/foo/{bar}/{foo}\")] // valid\r\n    [InlineData(\"/yahoo/foo/{bar}\", \"/foo/{bar}/{bar}\", \"DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder\")] // invalid\r\n    [InlineData(\"/yahoo/{foo}/{bar}\", \"/foo/{bar}/{bar}\", \"DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder\")] // invalid\r\n    public async Task IsPlaceholderNotDuplicatedIn_RuleForFileRoute_PathTemplatePlaceholdersAreValidated(string upstream, string downstream, params string[] expected)\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute(upstream, downstream);\n        GivenAConfiguration(route);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenThereAreErrors(expected.Length > 0);\n        ThenTheErrorMessagesAre(expected);\r\n    }\r\n\n    [Theory]\r\n    [Trait(\"PR\", \"1927\")]\n    [Trait(\"Bug\", \"683\")]\n    [InlineData(\"/foo/bar/{everything}/{everything}\", \"/bar/{everything}\", \"foo\", \"UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder\")]\r\n    [InlineData(\"/foo/bar/{everything}/{everything}\", \"/bar/{everything}/{everything}\", \"foo\", \"UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder\", \"DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder\")]\r\n    public async Task Configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template(string upstream, string downstream, string host, params string[] expected)\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute(upstream, downstream, upstreamHost: host);\n        GivenAConfiguration(route);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsNotValid();\n        ThenTheErrorMessagesAre(expected);\r\n    }\r\n\n    [Theory]\r\n    [Trait(\"PR\", \"1927\")]\n    [Trait(\"Bug\", \"683\")]\n    [InlineData(\"/foo/bar/{everything}\", \"/bar/{everything}/{everything}\", \"foo\", \"DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder\")]\r\n    [InlineData(\"/foo/bar/{everything}/{everything}\", \"/bar/{everything}/{everything}\", \"foo\", \"UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder\", \"DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder\")]\r\n    public async Task Configuration_is_invalid_when_placeholder_is_used_twice_in_downstream_path_template(string upstream, string downstream, string host, params string[] expected)\r\n    {\r\n        // Arrange\n        var route = GivenDefaultRoute(upstream, downstream, upstreamHost: host);\n        GivenAConfiguration(route);\n\n        // Act\n        await WhenIValidateTheConfiguration();\n\n        // Assert\n        ThenTheResultIsNotValid();\n        ThenTheErrorMessagesAre(expected);\r\n    }\r\n\r\n    #region PR 2114\r\n    [Fact]\r\n    [Trait(\"PR\", \"2114\")] // https://github.com/ThreeMammals/Ocelot/pull/2114\r\n    [Trait(\"Feat\", \"842\")] // https://github.com/ThreeMammals/Ocelot/issues/842\r\n    public async Task Configuration_is_not_valid_if_specified_authentication_provider_is_not_registered()\r\n    {\r\n        const string key = \"JwtLads\";\r\n        GivenConfigurationWithAuthenticationKey(key);\r\n        await WhenIValidateTheConfiguration();\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[] is unsupported authentication provider\");\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"2114\")]\r\n    [Trait(\"Feat\", \"842\")]\r\n    public async Task Configuration_is_valid_if_specified_authentication_provider_is_registered()\r\n    {\r\n        const string key = \"JwtLads\";\r\n        GivenConfigurationWithAuthenticationKey(key);\r\n        GivenTheAuthSchemeExists(key);\r\n        await WhenIValidateTheConfiguration();\r\n        ThenTheResultIsValid();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"2114\")]\r\n    [Trait(\"Feat\", \"842\")]\r\n    public async Task Configuration_is_not_valid_if_one_authentication_provider_is_not_registered()\r\n    {\r\n        string[] keys = { \"JwtLads\", \"other\" };\r\n        GivenConfigurationWithAuthenticationKeys(keys);\r\n        await WhenIValidateTheConfiguration();\r\n        ThenTheResultIsNotValid();\r\n        ThenTheErrorMessageAtPositionIs(0, \"AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'',AuthenticationProviderKeys:['JwtLads','other'] is unsupported authentication provider\");\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"PR\", \"2114\")]\r\n    [Trait(\"Feat\", \"842\")]\r\n    public async Task Configuration_is_valid_if_all_specified_authentication_provider_are_registered()\r\n    {\r\n        string[] keys = { \"JwtLads\", \"other\" };\r\n        GivenConfigurationWithAuthenticationKeys(keys);\r\n        GivenTheAuthSchemesExists(keys);\r\n        await WhenIValidateTheConfiguration();\r\n        ThenTheResultIsValid();\r\n    }\r\n    #endregion\r\n\r\n    private static FileRoute GivenDefaultRoute() => GivenDefaultRoute(null, null);\n    private static FileRoute GivenDefaultRoute(string upstream, string downstream,\r\n        string key = null, string[] methods = null, string upstreamHost = null) => new()\n    {\r\n        UpstreamHost = upstreamHost,\n        UpstreamHttpMethod = methods is null || methods.Length == 0 ? [HttpMethods.Get] : [..methods],\n        UpstreamPathTemplate = upstream ?? \"/asdf/\",\n        DownstreamPathTemplate = downstream ?? \"/api/products/\",\n        DownstreamHostAndPorts = new()\n        {\r\n            new(\"bbc.co.uk\", 12345),\r\n        },\n        DownstreamScheme = Uri.UriSchemeHttp,\r\n        Key = key,\r\n    };\r\n\n    private static FileRoute GivenServiceDiscoveryRoute() => new()\n    {\n        UpstreamHttpMethod = [HttpMethods.Get],\n        UpstreamPathTemplate = \"/laura\",\n        DownstreamPathTemplate = \"/\",\n        DownstreamScheme = Uri.UriSchemeHttp,\n        ServiceName = \"test\",\n    };\n\n    private static FileRoute GivenRouteWithUpstreamHeaderTemplates(string upstream, string downstream, Dictionary<string, string> templates) => new()\n    {\n        UpstreamPathTemplate = upstream,\n        DownstreamPathTemplate = downstream,\n        DownstreamHostAndPorts = new()\n        {\n            new(\"bbc.co.uk\", 123),\n        },\n        UpstreamHttpMethod = [HttpMethods.Get],\n        UpstreamHeaderTemplates = templates,\n    };\r\n\r\n    private void GivenAConfiguration(FileConfiguration fileConfiguration) => _fileConfiguration = fileConfiguration;\r\n\n    private FileConfiguration GivenAConfiguration(params FileRoute[] routes)\r\n    {\n        var config = new FileConfiguration();\n        config.Routes.AddRange(routes);\r\n        _fileConfiguration = config;\n        return config;\r\n    }\r\n\r\n    private void GivenConfigurationWithAuthenticationKey(string key)\r\n    {\r\n        _fileConfiguration = new FileConfiguration();\r\n        _fileConfiguration.GlobalConfiguration.AuthenticationOptions.AuthenticationProviderKey = key;\r\n    }\r\n\r\n    private void GivenConfigurationWithAuthenticationKeys(string[] keys)\r\n    {\r\n        _fileConfiguration = new FileConfiguration();\r\n        _fileConfiguration.GlobalConfiguration.AuthenticationOptions.AuthenticationProviderKeys = keys;\r\n    }\r\n\r\n    private static FileServiceDiscoveryProvider GivenDefaultServiceDiscoveryProvider() => new()\n    {\n        Scheme = Uri.UriSchemeHttps,\n        Host = \"localhost\",\n        Type = \"ServiceFabric\",\n        Port = 8500,\n    };\r\n\r\n    private async Task WhenIValidateTheConfiguration()\r\n        => _result = await _configurationValidator.IsValid(_fileConfiguration);\r\n\r\n    private void ThenTheResultIsValid()\r\n        => _result.Data.IsError.ShouldBeFalse();\r\n\r\n    private void ThenTheResultIsNotValid()\r\n        => _result.Data.IsError.ShouldBeTrue();\r\n\r\n    private void ThenTheErrorIs<T>()\r\n        => _result.Data.Errors[0].ShouldBeOfType<T>();\r\n\r\n    private void ThenTheErrorMessageAtPositionIs(int index, string expected)\r\n        => _result.Data.Errors[index].Message.ShouldBe(expected);\n\n    private void ThenThereAreErrors(bool isError)\r\n        => _result.Data.IsError.ShouldBe(isError);\n\n    private void ThenTheErrorMessagesAre(IEnumerable<string> messages)\r\n    {\n        _result.Data.Errors.Count.ShouldBe(messages.Count());\n\n        foreach (var msg in messages)\n        {\n            _result.Data.Errors.ShouldContain(e => e.Message == msg);\n        }\n    }\r\n\r\n    private void GivenTheAuthSchemeExists(string name)\r\n    {\r\n        _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List<AuthenticationScheme>\r\n        {\r\n            new(name, name, typeof(TestHandler)),\r\n        });\r\n    }\r\n\r\n    private void GivenTheAuthSchemesExists(string[] names)\r\n    {\r\n        _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(names.Select(n => new AuthenticationScheme(n, n, typeof(TestHandler))));\r\n    }\r\n\r\n    private void GivenAQoSHandler()\r\n    {\r\n        static DelegatingHandler Del(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler();\r\n        _services.AddSingleton((QosDelegatingHandlerDelegate)Del);\r\n        _provider = _services.BuildServiceProvider(true);\r\n        _configurationValidator = new FileConfigurationFluentValidator(\r\n            _provider,\r\n            new(new(), new(_provider), _fileAuthOptsValidator),\r\n            new(new(_provider), _fileAuthOptsValidator));\r\n    }\r\n\r\n    private void GivenAServiceDiscoveryHandler()\r\n    {\r\n        static IServiceDiscoveryProvider del(IServiceProvider a, ServiceProviderConfiguration b, DownstreamRoute c) => new FakeServiceDiscoveryProvider();\r\n        _services.AddSingleton((ServiceDiscoveryFinderDelegate)del);\r\n        _provider = _services.BuildServiceProvider(true);\r\n        _configurationValidator = new FileConfigurationFluentValidator(\r\n            _provider,\r\n            new(new(), new(_provider), _fileAuthOptsValidator),\r\n            new(new(_provider), _fileAuthOptsValidator));\r\n    }\r\n\r\n    private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider\r\n    {\r\n        public Task<List<Service>> GetAsync() => Task.FromResult<List<Service>>(new());\r\n    }\r\n\r\n    private class TestOptions : AuthenticationSchemeOptions { }\r\n\r\n    private class TestHandler : AuthenticationHandler<TestOptions>\r\n    {\n        // https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/isystemclock-obsolete\n        // .NET 8.0: TimeProvider is now a settable property on the Options classes for the authentication and identity components.\r\n        // It can be set directly or by registering a provider in the dependency injection container.\n        public TestHandler(IOptionsMonitor<TestOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)\r\n        { }\n\n        protected override Task<AuthenticateResult> HandleAuthenticateAsync()\r\n        {\r\n            var principal = new ClaimsPrincipal();\r\n            return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.File;\r\nusing Ocelot.Configuration.Validator;\r\nusing Ocelot.Logging;\r\nusing Ocelot.QualityOfService;\r\n\r\nnamespace Ocelot.UnitTests.Configuration.Validation;\r\n\r\npublic class FileQoSOptionsFluentValidatorTests : UnitTest\r\n{\r\n    private FileQoSOptionsFluentValidator _validator;\r\n    private readonly ServiceCollection _services;\r\n\r\n    public FileQoSOptionsFluentValidatorTests()\r\n    {\r\n        _services = new ServiceCollection();\r\n        var provider = _services.BuildServiceProvider(true);\r\n        _validator = new FileQoSOptionsFluentValidator(provider);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_be_valid_as_nothing_set()\r\n    {\r\n        // Arrange\r\n        var qosOptions = new FileQoSOptions();\r\n\r\n        // Act\r\n        var result = _validator.Validate(qosOptions);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_be_valid_as_qos_delegate_set()\r\n    {\r\n        // Arrange\r\n        var qosOptions = new FileQoSOptions\r\n        {\r\n            TimeoutValue = 1,\r\n            ExceptionsAllowedBeforeBreaking = 1,\r\n        };\r\n        GivenAQosDelegate();\r\n\r\n        // Act\r\n        var result = _validator.Validate(qosOptions);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_be_invalid_as_no_qos_delegate()\r\n    {\r\n        // Arrange\r\n        var qosOptions = new FileQoSOptions\r\n        {\r\n            TimeoutValue = 1,\r\n            ExceptionsAllowedBeforeBreaking = 1,\r\n        };\r\n\r\n        // Act\r\n        var result = _validator.Validate(qosOptions);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.Errors[0].ErrorMessage.ShouldBe(\"Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?\");\r\n    }\r\n\r\n    private void GivenAQosDelegate()\r\n    {\r\n        static DelegatingHandler Fake(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => null;\r\n        _services.AddSingleton((QosDelegatingHandlerDelegate)Fake);\r\n        var provider = _services.BuildServiceProvider(true);\r\n        _validator = new FileQoSOptionsFluentValidator(provider);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs",
    "content": "using Ocelot.Configuration.File;\r\nusing Ocelot.Configuration.Validator;\n\r\nnamespace Ocelot.UnitTests.Configuration.Validation;\r\n\r\npublic class HostAndPortValidatorTests : UnitTest\r\n{\r\n    private readonly HostAndPortValidator _validator;\r\n\r\n    public HostAndPortValidatorTests()\r\n    {\r\n        _validator = new HostAndPortValidator();\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(null)]\r\n    [InlineData(\"\")]\r\n    public void Should_be_invalid_because_host_empty(string host)\r\n    {\r\n        // Arrange\r\n        var hostAndPort = new FileHostAndPort\r\n        {\r\n            Host = host,\r\n        };\r\n\r\n        // Act\r\n        var result = _validator.Validate(hostAndPort);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.Errors[0].ErrorMessage.ShouldBe(\"When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_be_valid_because_host_set()\r\n    {\r\n        // Arrange\r\n        var hostAndPort = new FileHostAndPort\r\n        {\r\n            Host = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = _validator.Validate(hostAndPort);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs",
    "content": "﻿using FluentValidation.Results;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Validator;\nusing System.Reflection;\n\r\nnamespace Ocelot.UnitTests.Configuration.Validation;\r\n\r\npublic class RouteFluentValidatorTests : UnitTest\r\n{\r\n    private readonly RouteFluentValidator _validator;\r\n    private readonly Mock<IAuthenticationSchemeProvider> _authProvider;\r\n    private readonly Mock<IServiceProvider> _serviceProvider;\r\n\r\n    public RouteFluentValidatorTests()\r\n    {\r\n        _authProvider = new Mock<IAuthenticationSchemeProvider>();\r\n        _serviceProvider = new Mock<IServiceProvider>();\r\n\r\n        // TODO - replace with mocks\r\n        _validator = new RouteFluentValidator(\r\n            new HostAndPortValidator(),\r\n            new FileQoSOptionsFluentValidator(_serviceProvider.Object),\r\n            new FileAuthenticationOptionsValidator(_authProvider.Object));\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Downstream_path_template_should_not_be_empty()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute();\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Downstream Path Template cannot be empty\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Upstream_path_template_should_not_be_empty()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Upstream Path Template cannot be empty\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Downstream_path_template_should_start_with_forward_slash()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Downstream Path Template test doesnt start with forward slash\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Downstream_path_template_should_not_contain_double_forward_slash()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"//test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Downstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(\"https://test\")]\r\n    [InlineData(\"http://test\")]\r\n    [InlineData(\"/test/http://\")]\r\n    [InlineData(\"/test/https://\")]\r\n    public async Task Downstream_path_template_should_not_contain_scheme(string downstreamPathTemplate)\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = downstreamPathTemplate,\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains($\"Downstream Path Template {downstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Upstream_path_template_should_start_with_forward_slash()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Upstream Path Template test doesnt start with forward slash\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Upstream_path_template_should_not_contain_double_forward_slash()\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"//test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"Upstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(\"https://test\")]\r\n    [InlineData(\"http://test\")]\r\n    [InlineData(\"/test/http://\")]\r\n    [InlineData(\"/test/https://\")]\r\n    public async Task Upstream_path_template_should_not_contain_scheme(string upstreamPathTemplate)\r\n    {\r\n        // Arrange\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = upstreamPathTemplate,\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains($\"Upstream Path Template {upstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.\");\r\n    }\r\n\r\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, true, null)]\n    [InlineData(-1L, false, \"RateLimitOptions.Limit is negative or zero for the route /test\")]\n    [InlineData(0L, false, \"RateLimitOptions.Limit is negative or zero for the route /test\")]\r\n    [InlineData(1L, true, null)]\r\n    public async Task ShouldValidate_FileRateLimitByHeaderRule_Limit(long? limit, bool valid, string errorMessage)\r\n    {\r\n        // Arrange\r\n        var route = GivenRoute();\r\n        route.RateLimitOptions = new()\r\n        {\r\n            Limit = limit,\r\n            Period = \"1s\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.IsValid.ShouldBe(valid);\r\n        if (!result.IsValid)\r\n            result.ThenSingleErrorIs(errorMessage);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_not_be_valid_if_enable_rate_limiting_true_and_period_is_empty()\r\n    {\r\n        // Arrange\r\n        var route = GivenRoute();\r\n        route.RateLimitOptions = new()\r\n        {\r\n            EnableRateLimiting = true,\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.ThenTheErrorsContains(\"RateLimitOptions.Period is empty\");\r\n        result.ThenTheErrorsContains(\"RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_not_be_valid_if_enable_rate_limiting_true_and_period_has_value()\r\n    {\r\n        // Arrange\r\n        var route = GivenRoute();\r\n        route.RateLimitOptions = new()\r\n        {\r\n            Period = \"test\",\r\n        };\r\n\r\n        // Act\r\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\r\n        result.ThenSingleErrorIs(\"RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period\");\r\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\r\n    [Trait(\"PR\", \"2294\")]\r\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    [InlineData(\"1s\", true)]\n    [InlineData(\"12.34s\", true)]\n    [InlineData(\"1ms\", true)]\n    [InlineData(\"12.34ms\", true)]\n    [InlineData(\"2m\", true)]\n    [InlineData(\"23.45m\", true)]\n    [InlineData(\"3h\", true)]\n    [InlineData(\"34.56h\", true)]\n    [InlineData(\"4d\", true)]\n    [InlineData(\"4.5d\", true)]\n    [InlineData(\"123\", false)]\n    [InlineData(\"-123\", false)]\n    [InlineData(\"bad\", false)]\n    [InlineData(\" 3s \", true)]\n    [InlineData(\" -3s \", false)]\n    public void IsValidPeriod_ReflectionLifeHack_BranchesAreCovered(string period, bool expected)\n    {\n        // Arrange\n        var method = _validator.GetType().GetMethod(\"IsValidPeriod\", BindingFlags.NonPublic | BindingFlags.Static);\n        var argument = new FileRateLimitByHeaderRule { Period = period };\n\n        // Act\n        bool actual = (bool)method.Invoke(_validator, new object[] { argument });\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_not_be_valid_if_specified_authentication_provider_isnt_registered()\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            AuthenticationOptions = new FileAuthenticationOptions\r\n            {\r\n                AuthenticationProviderKey = \"JwtLads\",\r\n            },\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[] is unsupported authentication provider\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_not_be_valid_if_not_using_service_discovery_and_no_host_and_ports()\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_be_valid_if_using_service_discovery_and_no_host_and_ports()\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            ServiceName = \"Lads\",\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_be_valid_re_route_using_host_and_port_and_paths()\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            DownstreamHostAndPorts = new List<FileHostAndPort>\r\n            {\r\n                new()\r\n                {\r\n                    Host = \"localhost\",\r\n                    Port = 5000,\r\n                },\r\n            },\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_be_valid_if_specified_authentication_provider_is_registered()\r\n    {\r\n        // Arrange\n        const string key = \"JwtLads\";\r\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            AuthenticationOptions = new FileAuthenticationOptions\r\n            {\r\n                AuthenticationProviderKey = key,\r\n            },\r\n            DownstreamHostAndPorts = new List<FileHostAndPort>\r\n            {\r\n                new()\r\n                {\r\n                    Host = \"localhost\",\r\n                    Port = 5000,\r\n                },\r\n            },\r\n        };\r\n        GivenAnAuthProvider(key);\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(\"1.0\")]\r\n    [InlineData(\"1.1\")]\r\n    [InlineData(\"2.0\")]\r\n    [InlineData(\"1,0\")]\r\n    [InlineData(\"1,1\")]\r\n    [InlineData(\"2,0\")]\r\n    [InlineData(\"1\")]\r\n    [InlineData(\"2\")]\r\n    [InlineData(\"\")]\r\n    [InlineData(null)]\r\n    public async Task Should_be_valid_re_route_using_downstream_http_version(string version)\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            DownstreamHostAndPorts = new List<FileHostAndPort>\r\n            {\r\n                new()\r\n                {\r\n                    Host = \"localhost\",\r\n                    Port = 5000,\r\n                },\r\n            },\r\n            DownstreamHttpVersion = version,\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeTrue();\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(\"retg1.1\")]\r\n    [InlineData(\"re2.0\")]\r\n    [InlineData(\"1,0a\")]\r\n    [InlineData(\"a1,1\")]\r\n    [InlineData(\"12,0\")]\r\n    [InlineData(\"asdf\")]\r\n    public async Task Should_be_invalid_re_route_using_downstream_http_version(string version)\r\n    {\r\n        // Arrange\n        var route = new FileRoute\r\n        {\r\n            DownstreamPathTemplate = \"/test\",\r\n            UpstreamPathTemplate = \"/test\",\r\n            DownstreamHostAndPorts = new List<FileHostAndPort>\r\n            {\r\n                new()\r\n                {\r\n                    Host = \"localhost\",\r\n                    Port = 5000,\r\n                },\r\n            },\r\n            DownstreamHttpVersion = version,\r\n        };\r\n\r\n        // Act\n        var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken);\r\n\r\n        // Assert\n        result.IsValid.ShouldBeFalse();\r\n        result.ThenTheErrorsContains(\"'Downstream Http Version'\"); // this error message changes depending on the OS language\r\n    }\r\n\r\n    private static FileRoute GivenRoute() => new()\r\n    {\r\n        DownstreamPathTemplate = \"/test\",\r\n        UpstreamPathTemplate = \"/test\",\r\n        DownstreamHostAndPorts = new()\r\n        {\r\n            new(\"localhost\", 5000),\r\n        },\r\n    };\r\n\r\n    private void GivenAnAuthProvider(string key)\r\n    {\r\n        var schemes = new List<AuthenticationScheme>\r\n        {\r\n            new(key, key, typeof(FakeAutheHandler)),\r\n        };\r\n\r\n        _authProvider\r\n            .Setup(x => x.GetAllSchemesAsync())\r\n            .ReturnsAsync(schemes);\r\n    }\r\n\r\n    private class FakeAutheHandler : IAuthenticationHandler\r\n    {\r\n        public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)\r\n        {\r\n            throw new System.NotImplementedException();\r\n        }\r\n\r\n        public Task<AuthenticateResult> AuthenticateAsync()\r\n        {\r\n            throw new System.NotImplementedException();\r\n        }\r\n\r\n        public Task ChallengeAsync(AuthenticationProperties properties)\r\n        {\r\n            throw new System.NotImplementedException();\r\n        }\r\n\r\n        public Task ForbidAsync(AuthenticationProperties properties)\r\n        {\r\n            throw new System.NotImplementedException();\r\n        }\r\n    }\r\n}\r\n\r\nstatic class ValidationResultExtensions\r\n{\r\n    public static void ThenTheErrorsContains(this ValidationResult result, string expected)\r\n        => result.Errors.ShouldContain(x => x.ErrorMessage.Contains(expected));\r\n\r\n    public static void ThenSingleErrorIs(this ValidationResult result, string message)\r\n    {\r\n        Assert.False(result.IsValid);\r\n        Assert.Single(result.Errors);\r\n        Assert.Equal(message, result.Errors[0].ErrorMessage);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Creator;\n\nnamespace Ocelot.UnitTests.Configuration;\n\npublic class VersionCreatorTests : UnitTest\n{\n    private readonly HttpVersionCreator _creator = new();\n\n    [Fact]\n    public void Should_create_version_based_on_input()\n    {\n        // Arrange, Act\n        var result = _creator.Create(\"2.0\");\n\n        // Assert\n        result.Major.ShouldBe(2);\n        result.Minor.ShouldBe(0);\n    }\n\n    [Fact]\n    public void Should_default_to_version_one_point_one()\n    {\n        // Arrange, Act\n        var result = _creator.Create(string.Empty);\n\n        // Assert\n        result.Major.ShouldBe(1);\n        result.Minor.ShouldBe(1);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/AgentServiceExtensions.cs",
    "content": "﻿using Consul;\n\nnamespace Ocelot.UnitTests.Consul;\n\ninternal static class AgentServiceExtensions\n{\n    public static AgentService WithServiceName(this AgentService agent, string serviceName)\n    {\n        agent.Service = serviceName;\n        return agent;\n    }\n\n    public static AgentService WithPort(this AgentService agent, int port)\n    {\n        agent.Port = port;\n        return agent;\n    }\n\n    public static AgentService WithAddress(this AgentService agent, string address)\n    {\n        agent.Address = address;\n        return agent;\n    }\n\n    public static ServiceEntry ToServiceEntry(this AgentService agent) => new()\n    {\n        Service = agent,\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.Extensions.Options;\nusing Newtonsoft.Json;\nusing Ocelot.Cache;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.Responses;\nusing System.Text;\n\r\nnamespace Ocelot.UnitTests.Consul;\r\n\r\npublic class ConsulFileConfigurationRepositoryTests : UnitTest\r\n{\r\n    private ConsulFileConfigurationRepository _repo;\r\n    private readonly Mock<IOptions<FileConfiguration>> _options;\r\n    private readonly Mock<IOcelotCache<FileConfiguration>> _cache;\r\n    private readonly Mock<IConsulClientFactory> _factory;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n    private readonly Mock<IConsulClient> _client;\r\n    private readonly Mock<IKVEndpoint> _kvEndpoint;\r\n    private FileConfiguration _fileConfiguration;\r\n    private Response<FileConfiguration> _getResult;\r\n\r\n    public ConsulFileConfigurationRepositoryTests()\r\n    {\r\n        _cache = new Mock<IOcelotCache<FileConfiguration>>();\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n\r\n        _options = new Mock<IOptions<FileConfiguration>>();\r\n        _factory = new Mock<IConsulClientFactory>();\r\n        _client = new Mock<IConsulClient>();\r\n        _kvEndpoint = new Mock<IKVEndpoint>();\r\n        _client\r\n            .Setup(x => x.KV)\r\n            .Returns(_kvEndpoint.Object);\r\n        _factory\r\n            .Setup(x => x.Get(It.IsAny<ConsulRegistryConfiguration>()))\r\n            .Returns(_client.Object);\r\n        _options\r\n            .SetupGet(x => x.Value)\r\n            .Returns(() => _fileConfiguration);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_set_config()\r\n    {\r\n        // Arrange\r\n        var config = GivenFakeFileConfiguration();\r\n        GivenWritingToConsulSucceeds();\r\n\r\n        // Act\r\n        _ = await _repo.Set(config);\r\n\r\n        // Assert\r\n        ThenTheConfigurationIsStoredAs(config);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_get_config()\r\n    {\r\n        // Arrange\r\n        var config = _fileConfiguration = GivenFakeFileConfiguration();\r\n        GivenFetchFromConsulSucceeds();\r\n\r\n        // Act\r\n        _getResult = await _repo.Get();\r\n\r\n        // Assert\r\n        ThenTheConfigurationIs(config);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_get_null_config()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = GivenFakeFileConfiguration();\r\n        GivenFetchFromConsulReturnsNull();\r\n\r\n        // Act\r\n        _getResult = await _repo.Get();\r\n\r\n        // Assert\r\n        _getResult.Data.ShouldBeNull();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_get_config_from_cache()\r\n    {\r\n        // Arrange\r\n        var config = _fileConfiguration = GivenFakeFileConfiguration();\r\n        GivenFetchFromCacheSucceeds();\r\n\r\n        // Act\r\n        _getResult = await _repo.Get();\r\n\r\n        // Assert\r\n        ThenTheConfigurationIs(config);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_set_config_key()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = GivenFakeFileConfiguration();\r\n        GivenTheConfigKeyComesFromFileConfig(\"Tom\");\r\n        GivenFetchFromConsulSucceeds();\r\n\r\n        // Act\r\n        _getResult = await _repo.Get();\r\n\r\n        // Assert\r\n        ThenTheConfigKeyIs(\"Tom\");\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_set_default_config_key()\r\n    {\r\n        // Arrange\r\n        _fileConfiguration = GivenFakeFileConfiguration();\r\n        GivenFetchFromConsulSucceeds();\r\n\r\n        // Act\r\n        _getResult = await _repo.Get();\r\n\r\n        // Assert\r\n        ThenTheConfigKeyIs(\"InternalConfiguration\");\r\n    }\r\n\r\n    private void ThenTheConfigKeyIs(string expected)\r\n    {\r\n        _kvEndpoint.Verify(x => x.Get(expected, It.IsAny<CancellationToken>()), Times.Once);\r\n    }\r\n\r\n    private void GivenTheConfigKeyComesFromFileConfig(string key)\r\n    {\r\n        _fileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = key;\r\n        _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object);\r\n    }\r\n\r\n    private void ThenTheConfigurationIs(FileConfiguration config)\r\n    {\r\n        var expected = JsonConvert.SerializeObject(config, Formatting.Indented);\r\n        var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented);\r\n        result.ShouldBe(expected);\r\n    }\r\n\r\n    private void GivenWritingToConsulSucceeds()\r\n    {\r\n        var response = new WriteResult<bool>\r\n        {\r\n            Response = true,\r\n        };\r\n        _kvEndpoint.Setup(x => x.Put(It.IsAny<KVPair>(), It.IsAny<CancellationToken>())).ReturnsAsync(response);\r\n    }\r\n\r\n    private void GivenFetchFromCacheSucceeds()\r\n    {\r\n        _cache.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())).Returns(_fileConfiguration);\r\n    }\r\n\r\n    private void GivenFetchFromConsulReturnsNull()\r\n    {\r\n        var result = new QueryResult<KVPair>();\r\n        _kvEndpoint.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))\r\n            .ReturnsAsync(result);\r\n    }\r\n\r\n    private void GivenFetchFromConsulSucceeds()\r\n    {\r\n        var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented);\r\n        var bytes = Encoding.UTF8.GetBytes(json);\r\n        var kvp = new KVPair(\"OcelotConfiguration\")\r\n        {\r\n            Value = bytes,\r\n        };\r\n        var query = new QueryResult<KVPair>\r\n        {\r\n            Response = kvp,\r\n        };\r\n        _kvEndpoint.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))\r\n            .ReturnsAsync(query);\r\n    }\r\n\r\n    private void ThenTheConfigurationIsStoredAs(FileConfiguration config)\r\n    {\r\n        var json = JsonConvert.SerializeObject(config, Formatting.Indented);\r\n        var bytes = Encoding.UTF8.GetBytes(json);\r\n        _kvEndpoint.Verify(x => x.Put(It.Is<KVPair>(k => k.Value.SequenceEqual(bytes)), It.IsAny<CancellationToken>()), Times.Once);\r\n    }\r\n\r\n    private FileConfiguration GivenFakeFileConfiguration()\r\n    {\r\n        var routes = new List<FileRoute>\r\n        {\r\n            new()\r\n            {\r\n                DownstreamHostAndPorts = new List<FileHostAndPort>\r\n                {\r\n                    new(\"123.12.12.12\", 80),\r\n                },\r\n                DownstreamScheme = Uri.UriSchemeHttps,\r\n                DownstreamPathTemplate = \"/asdfs/test/{test}\",\r\n            },\r\n        };\r\n        var globalConfiguration = new FileGlobalConfiguration\r\n        {\r\n            ServiceDiscoveryProvider = new FileServiceDiscoveryProvider\r\n            {\r\n                Scheme = Uri.UriSchemeHttps,\r\n                Port = 198,\r\n                Host = \"blah\",\r\n            },\r\n        };\r\n        _fileConfiguration = new FileConfiguration\r\n        {\r\n            GlobalConfiguration = globalConfiguration,\r\n            Routes = routes,\r\n        };\r\n        _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object);\r\n        return _fileConfiguration;\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/ConsulProviderFactoryTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.UnitTests.Consul;\n\npublic sealed class ConsulProviderFactoryTests : UnitTest, IDisposable\n{\n    private readonly ServiceProvider _provider;\n    private readonly IServiceScope _scope;\n    private readonly DefaultHttpContext _context = new();\n\n    public ConsulProviderFactoryTests()\n    {\n        var contextAccessor = new Mock<IHttpContextAccessor>();\n        _context.Items.Add(nameof(ConsulRegistryConfiguration), new ConsulRegistryConfiguration(null, null, 0, null, null));\n        contextAccessor.SetupGet(x => x.HttpContext).Returns(_context);\n\n        var loggerFactory = new Mock<IOcelotLoggerFactory>();\n        var logger = new Mock<IOcelotLogger>();\n        loggerFactory.Setup(x => x.CreateLogger<Provider.Consul.Consul>()).Returns(logger.Object);\n        loggerFactory.Setup(x => x.CreateLogger<PollConsul>()).Returns(logger.Object);\n\n        var consulFactory = new Mock<IConsulClientFactory>();\n        var consulServiceBuilder = new Mock<IConsulServiceBuilder>();\n\n        var services = new ServiceCollection();\n        services.AddSingleton(contextAccessor.Object);\n        services.AddSingleton(consulFactory.Object);\n        services.AddSingleton(loggerFactory.Object);\n        services.AddScoped(_ => consulServiceBuilder.Object);\n\n        _provider = services.BuildServiceProvider(true); // validate scopes!!!\n        _scope = _provider.CreateScope();\n        _context.RequestServices = _scope.ServiceProvider;\n    }\n\n    public void Dispose()\n    {\n        _scope.Dispose();\n        _provider.Dispose();\n    }\n\n    [Fact]\n    public void Get_EmptyTypeName_ReturnedConsul()\n    {\n        // Arrange\n        var emptyType = string.Empty;\n        var route = GivenRoute(string.Empty);\n\n        // Act\n        var actual = CreateProvider(route, emptyType);\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeOfType<Provider.Consul.Consul>();\n    }\n\n    [Fact]\n    public void Get_PollConsulTypeName_ReturnedPollConsul()\n    {\n        // Arrange, Act\n        var route = GivenRoute(string.Empty);\n        var actual = CreateProvider(route, nameof(PollConsul));\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeOfType<PollConsul>();\n    }\n\n    [Fact]\n    public void Get_RoutesWithTheSameServiceName_ReturnedSameProvider()\n    {\n        // Arrange, Act: 1\n        var route1 = GivenRoute(\"test\");\n        var actual1 = CreateProvider(route1);\n\n        // Arrange, Act: 2\n        var route2 = GivenRoute(\"test\");\n        var actual2 = CreateProvider(route2);\n\n        // Assert\n        actual1.ShouldNotBeNull().ShouldBeOfType<PollConsul>();\n        actual2.ShouldNotBeNull().ShouldBeOfType<PollConsul>();\n        actual1.ShouldBeEquivalentTo(actual2);\n        var provider1 = actual1 as PollConsul;\n        var provider2 = actual2 as PollConsul;\n        provider1.ServiceName.ShouldBeEquivalentTo(provider2.ServiceName);\n    }\n\n    [Fact]\n    public void Get_MultipleServiceNames_ShouldReturnProviderAccordingToServiceName()\n    {\n        string[] serviceNames = new[] { \"service1\", \"service2\", \"service3\", \"service4\" };\n        var providersList = serviceNames.Select(DummyPollingConsulServiceFactory).ToList();\n\n        foreach (var serviceName in serviceNames)\n        {\n            var currentProvider = DummyPollingConsulServiceFactory(serviceName);\n            providersList.ShouldContain(currentProvider);\n        }\n\n        var convertedProvidersList = providersList.Select(x => x as PollConsul).ToList();\n        convertedProvidersList.ForEach(x => x.ShouldNotBeNull());\n\n        foreach (var serviceName in serviceNames)\n        {\n            var cProvider = DummyPollingConsulServiceFactory(serviceName);\n            var convertedCProvider = cProvider as PollConsul;\n            convertedCProvider.ShouldNotBeNull();\n\n            var matchingProviders = convertedProvidersList\n                .Where(x => x.ServiceName == convertedCProvider.ServiceName)\n                .ToList();\n            matchingProviders.ShouldHaveSingleItem();\n\n            matchingProviders.First()\n                .ShouldNotBeNull()\n                .ServiceName.ShouldBeEquivalentTo(convertedCProvider.ServiceName);\n        }\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2178\")]\n    public void Get_RootProvider_ShouldThrowInvalidOperationException()\n    {\n        // Arrange\n        var route = GivenRoute(string.Empty);\n        _context.RequestServices = _provider; // given service provider is root provider\n\n        // Act\n        Func<IServiceDiscoveryProvider> consulProviderFactoryCall = () => CreateProvider(route);\n\n        // Assert\n        consulProviderFactoryCall.ShouldThrow<InvalidOperationException>();\n    }\n\n    private IServiceDiscoveryProvider DummyPollingConsulServiceFactory(string serviceName) => CreateProvider(GivenRoute(serviceName));\n\n    private static DownstreamRoute GivenRoute(string serviceName) => new DownstreamRouteBuilder()\n        .WithServiceName(serviceName)\n        .Build();\n\n    private IServiceDiscoveryProvider CreateProvider(DownstreamRoute route, string providerType = ConsulProviderFactory.PollConsul)\n    {\n        var stopsFromPolling = 10000;\n        return ConsulProviderFactory.Get.Invoke(\n            _provider,\n            new ServiceProviderConfiguration()\n            {\n                Type = providerType,\n                Scheme = Uri.UriSchemeHttp,\n                PollingInterval = stopsFromPolling,\n            }, \n            route);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing System.Reflection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.Consul;\n\npublic sealed class DefaultConsulServiceBuilderTests\n{\n    private DefaultConsulServiceBuilder sut;\n    private readonly Mock<IHttpContextAccessor> contextAccessor;\n    private readonly Mock<IConsulClientFactory> clientFactory;\n    private readonly Mock<IOcelotLoggerFactory> loggerFactory;\n    private readonly Mock<IOcelotLogger> logger;\n    private ConsulRegistryConfiguration _configuration;\n\n    public DefaultConsulServiceBuilderTests()\n    {\n        contextAccessor = new();\n        clientFactory = new();\n        clientFactory.Setup(x => x.Get(It.IsAny<ConsulRegistryConfiguration>()))\n            .Returns(new ConsulClient());\n        logger = new();\n        loggerFactory = new();\n        loggerFactory.Setup(x => x.CreateLogger<DefaultConsulServiceBuilder>())\n            .Returns(logger.Object);\n    }\n\n    private void Arrange([CallerMemberName] string testName = null)\n    {\n        _configuration = new(null, null, 0, testName, null);\n        var context = new DefaultHttpContext();\n        context.Items.Add(nameof(ConsulRegistryConfiguration), _configuration);\n        contextAccessor.SetupGet(x => x.HttpContext).Returns(context);\n        sut = new DefaultConsulServiceBuilder(contextAccessor.Object, clientFactory.Object, loggerFactory.Object);\n    }\n\n    [Fact]\n    public void Ctor_PrivateMembers_PropertiesAreInitialized()\n    {\n        Arrange();\n        var propClient = sut.GetType().GetProperty(\"Client\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var propLogger = sut.GetType().GetProperty(\"Logger\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var propConfiguration = sut.GetType().GetProperty(\"Configuration\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n        // Act\n        var actualConfiguration = propConfiguration.GetValue(sut);\n        var actualClient = propClient.GetValue(sut);\n        var actualLogger = propLogger.GetValue(sut);\n\n        // Assert\n        actualConfiguration.ShouldNotBeNull().ShouldBe(_configuration);\n        actualClient.ShouldNotBeNull();\n        actualLogger.ShouldNotBeNull();\n    }\n\n    private static Type Me { get; } = typeof(DefaultConsulServiceBuilder);\n    private static MethodInfo GetNode { get; } = Me.GetMethod(\"GetNode\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n    [Fact]\n    public void GetNode_EntryBranch_ReturnsEntryNode()\n    {\n        Arrange();\n        Node node = new() { Name = nameof(GetNode_EntryBranch_ReturnsEntryNode) };\n        ServiceEntry entry = new() { Node = node };\n\n        // Act\n        var actual = GetNode.Invoke(sut, new object[] { entry, null }) as Node;\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBe(node);\n        actual.Name.ShouldBe(node.Name);\n    }\n\n    [Fact]\n    public void GetNode_NodesBranch_ReturnsNodeFromCollection()\n    {\n        Arrange();\n        ServiceEntry entry = new()\n        {\n            Node = null,\n            Service = new() { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) },\n        };\n        Node[] nodes = null;\n\n        // Act, Assert: nodes is null\n        var actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node;\n        actual.ShouldBeNull();\n\n        // Arrange, Act, Assert: nodes has items, happy path\n        var node = new Node { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) };\n        nodes = new[] { node };\n        actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node;\n        actual.ShouldNotBeNull().ShouldBe(node);\n        actual.Address.ShouldBe(entry.Service.Address);\n\n        // Arrange, Act, Assert: nodes has items, some nulls in entry\n        entry.Service.Address = null;\n        actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node;\n        actual.ShouldBeNull();\n\n        entry.Service = null;\n        actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node;\n        actual.ShouldBeNull();\n\n        entry = null;\n        actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node;\n        actual.ShouldBeNull();\n    }\n\n    private static MethodInfo GetDownstreamHost { get; } = Me.GetMethod(\"GetDownstreamHost\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n    [Fact]\n    public void GetDownstreamHost_BothBranches_NameOrAddress()\n    {\n        Arrange();\n\n        // Arrange, Act, Assert: node branch\n        ServiceEntry entry = new()\n        {\n            Service = new() { Address = nameof(GetDownstreamHost_BothBranches_NameOrAddress) },\n        };\n        var node = new Node { Name = \"test1\" };\n        var actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string;\n        actual.ShouldNotBeNull().ShouldBe(\"test1\");\n\n        // Arrange, Act, Assert: entry branch\n        node = null;\n        actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string;\n        actual.ShouldNotBeNull().ShouldBe(nameof(GetDownstreamHost_BothBranches_NameOrAddress));\n    }\n\n    private static MethodInfo GetServiceVersion { get; } = Me.GetMethod(\"GetServiceVersion\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n    [Fact]\n    public void GetServiceVersion_TagsIsNull_EmptyString()\n    {\n        Arrange();\n\n        // Arrange, Act, Assert: collection is null\n        ServiceEntry entry = new()\n        {\n            Service = new() { Tags = null },\n        };\n        Node node = null;\n        var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string;\n        actual.ShouldBe(string.Empty);\n\n        // Arrange, Act, Assert: collection has no version tag\n        entry.Service.Tags = new[] { \"test\" };\n        actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string;\n        actual.ShouldBe(string.Empty);\n    }\n\n    [Fact]\n    public void GetServiceVersion_HasTags_HappyPath()\n    {\n        Arrange();\n\n        // Arrange\n        var tags = new string[] { \"test\", \"version-v2\" };\n        ServiceEntry entry = new()\n        {\n            Service = new() { Tags = tags },\n        };\n        Node node = null;\n\n        // Act\n        var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string;\n\n        // Assert\n        actual.ShouldBe(\"v2\");\n    }\n\n    private static MethodInfo GetServiceTags { get; } = Me.GetMethod(\"GetServiceTags\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n    [Fact]\n    public void GetServiceTags_BothBranches()\n    {\n        Arrange();\n\n        // Arrange, Act, Assert: collection is null\n        ServiceEntry entry = new()\n        {\n            Service = new() { Tags = null },\n        };\n        Node node = null;\n        var actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable<string>;\n        actual.ShouldNotBeNull().ShouldBeEmpty();\n\n        // Arrange, Act, Assert: happy path\n        entry.Service.Tags = new string[] { \"1\", \"2\", \"3\" };\n        actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable<string>;\n        actual.ShouldNotBeNull().ShouldNotBeEmpty();\n        actual.Count().ShouldBe(3);\n        actual.ShouldContain(\"3\");\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs",
    "content": "﻿using Consul;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Consul;\nusing Ocelot.Provider.Consul.Interfaces;\nusing Ocelot.Values;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Consul;\n\npublic class OcelotBuilderExtensionsTests : UnitTest\n{\n    private readonly IServiceCollection _services;\n    private readonly IConfiguration _configRoot;\n\n    public OcelotBuilderExtensionsTests()\n    {\n        _configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());\n        _services = new ServiceCollection();\n        _services.AddSingleton(GetHostingEnvironment());\n        _services.AddSingleton(_configRoot);\n    }\n\n    private static IWebHostEnvironment GetHostingEnvironment()\n    {\n        var environment = new Mock<IWebHostEnvironment>();\n        environment.Setup(e => e.ApplicationName)\n            .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name);\n        return environment.Object;\n    }\n\n    [Fact]\n    public void AddConsul_ShouldSetUpConsul()\n    {\n        // Arrange, Act\n        var builder = _services.AddOcelot(_configRoot)\n            .AddConsul();\n\n        // Assert\n        builder.ShouldNotBeNull();\n    }\n\n    [Fact]\n    public void AddConfigStoredInConsul_ShouldSetUpConsul()\n    {\n        // Arrange, Act\n        var builder = _services.AddOcelot(_configRoot)\n            .AddConsul()\n            .AddConfigStoredInConsul();\n\n        // Assert\n        builder.ShouldNotBeNull();\n    }\n\n    [Fact]\n    public void AddConsulGeneric_TServiceBuilder_ShouldSetUpConsul()\n    {\n        // Arrange, Act\n        var builder = _services\n            .AddOcelot(_configRoot)\n            .AddConsul<FakeConsulServiceBuilder>();\n\n        // Assert\n        builder.ShouldNotBeNull();\n        builder.Services.SingleOrDefault(s => s.ServiceType == typeof(IConsulServiceBuilder)).ShouldNotBeNull();\n    }\n}\n\ninternal class FakeConsulServiceBuilder : IConsulServiceBuilder\n{\n    public ConsulRegistryConfiguration Configuration => throw new NotImplementedException();\n    public IEnumerable<Service> BuildServices(ServiceEntry[] entries, Node[] nodes) => throw new NotImplementedException();\n    public Service CreateService(ServiceEntry serviceEntry, Node serviceNode) => throw new NotImplementedException();\n    public bool IsValid(ServiceEntry entry) => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs",
    "content": "﻿using Ocelot.Infrastructure;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Consul;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.Consul;\n\npublic class PollingConsulServiceDiscoveryProviderTests : UnitTest\n{\n    private readonly int _delay;\n    private readonly List<Service> _services;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IServiceDiscoveryProvider> _consulServiceDiscoveryProvider;\n    private List<Service> _result;\n\n    public PollingConsulServiceDiscoveryProviderTests()\n    {\n        _services = new List<Service>();\n        _delay = 1;\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _factory.Setup(x => x.CreateLogger<PollConsul>()).Returns(_logger.Object);\n        _consulServiceDiscoveryProvider = new Mock<IServiceDiscoveryProvider>();\n    }\n\n    [Fact]\n    public void Should_return_service_from_consul()\n    {\n        // Arrange\n        var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List<string>());\n        GivenConsulReturns(service);\n\n        // Act\n        WhenIGetTheServices(1);\n\n        // Assert\n        _result.Count.ShouldBe(1);\n    }\n\n    [Fact]\n    public async Task Should_return_service_from_consul_without_delay()\n    {\n        // Arrange\n        var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List<string>());\n        GivenConsulReturns(service);\n\n        // Act\n        await WhenIGetTheServicesWithoutDelay(1);\n\n        // Assert\n        _result.Count.ShouldBe(1);\n    }\n\n    private void GivenConsulReturns(Service service)\n    {\n        _services.Add(service);\n        _consulServiceDiscoveryProvider.Setup(x => x.GetAsync()).ReturnsAsync(_services);\n    }\n\n    private void WhenIGetTheServices(int expected)\n    {\n        var provider = new PollConsul(_delay, \"test\", _factory.Object, _consulServiceDiscoveryProvider.Object);\n        var result = Wait.For(3_000).Until(() =>\n        {\n            try\n            {\n                _result = provider.GetAsync().GetAwaiter().GetResult();\n                return _result.Count == expected;\n            }\n            catch (Exception)\n            {\n                return false;\n            }\n        });\n        result.ShouldBeTrue();\n    }\n\n    private async Task WhenIGetTheServicesWithoutDelay(int expected)\n    {\n        var provider = new PollConsul(_delay, \"test2\", _factory.Object, _consulServiceDiscoveryProvider.Object);\n        bool result;\n        try\n        {\n            _result = await provider.GetAsync();\n            result = _result.Count == expected;\n        }\n        catch (Exception)\n        {\n            result = false;\n        }\n\n        result.ShouldBeTrue();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs",
    "content": "using Microsoft.AspNetCore.Mvc;\nusing Ocelot.Administration;\nusing Ocelot.Configuration.File;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Configuration.Setter;\nusing Ocelot.Errors;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Controllers;\n\npublic class FileConfigurationControllerTests : UnitTest\n{\n    private readonly FileConfigurationController _controller;\n    private readonly Mock<IFileConfigurationRepository> _repo;\n    private readonly Mock<IFileConfigurationSetter> _setter;\n\n    public FileConfigurationControllerTests()\n    {\n        _repo = new Mock<IFileConfigurationRepository>();\n        _setter = new Mock<IFileConfigurationSetter>();\n        _controller = new FileConfigurationController(_repo.Object, _setter.Object);\n    }\n\n    [Fact]\n    public async Task Should_get_file_configuration()\n    {\n        // Arrange\n        var expected = new OkResponse<FileConfiguration>(new FileConfiguration());\n        _repo.Setup(x => x.Get()).ReturnsAsync(expected);\n\n        // Act\n        var result = await _controller.Get();\n\n        // Assert\n        _repo.Verify(x => x.Get(), Times.Once);\n    }\n\n    [Fact]\n    public async Task Should_return_error_when_cannot_get_config()\n    {\n        // Arrange\n        var expected = new ErrorResponse<FileConfiguration>(It.IsAny<Error>());\n        _repo.Setup(x => x.Get()).ReturnsAsync(expected);\n\n        // Act\n        var result = await _controller.Get();\n\n        // Assert\n        _repo.Verify(x => x.Get(), Times.Once);\n        result.ShouldBeOfType<BadRequestObjectResult>();\n    }\n\n    [Fact]\n    public async Task Should_post_file_configuration()\n    {\n        // Arrange\n        var expected = new FileConfiguration();\n        _setter.Setup(x => x.Set(It.IsAny<FileConfiguration>())).ReturnsAsync(new OkResponse());\n\n        // Act\n        var result = await _controller.Post(expected);\n\n        // Assert\n        _setter.Verify(x => x.Set(expected), Times.Once);\n    }\n\n    [Fact]\n    public async Task Should_return_error_when_cannot_set_config()\n    {\n        // Arrange\n        var expected = new FileConfiguration();\n        _setter.Setup(x => x.Set(It.IsAny<FileConfiguration>())).ReturnsAsync(new ErrorResponse(new FakeError()));\n\n        // Act\n        var result = await _controller.Post(expected);\n\n        // Assert\n        _setter.Verify(x => x.Set(expected), Times.Once);\n        result.ShouldBeOfType<BadRequestObjectResult>();\n    }\n\n    [Fact]\n    public async Task Should_catch_exception_when_cannot_set_config()\n    {\n        // Arrange\n        var expected = new FileConfiguration();\n        _setter.Setup(x => x.Set(It.IsAny<FileConfiguration>()))\n            .Throws(new Exception(\"Service failed\"));\n\n        // Act\n        var result = await _controller.Post(expected);\n\n        // Assert\n        _setter.Verify(x => x.Set(expected), Times.Once);\n        result.ShouldBeOfType<BadRequestObjectResult>();\n        var actual = result as BadRequestObjectResult;\n        Assert.StartsWith(\"Service failed:\", actual.Value.ToString());\n    }\n\n    private class FakeError : Error\n    {\n        public FakeError() : base(string.Empty, OcelotErrorCode.CannotAddDataError, 404)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs",
    "content": "using Microsoft.AspNetCore.Mvc;\nusing Ocelot.Administration;\nusing Ocelot.Cache;\n\r\nnamespace Ocelot.UnitTests.Controllers;\r\n\r\npublic class OutputCacheControllerTests : UnitTest\r\n{\r\n    private readonly OutputCacheController _controller;\r\n    private readonly Mock<IOcelotCache<CachedResponse>> _cache;\r\n\r\n    public OutputCacheControllerTests()\r\n    {\r\n        _cache = new();\r\n        _controller = new(_cache.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Delete_ByKey_ClearedRegion()\r\n    {\n        // Arrange, Act\r\n        var result = _controller.Delete(\"a\");\r\n\r\n        // Assert\r\n        result.ShouldBeOfType<NoContentResult>();\r\n        _cache.Verify(x => x.ClearRegion(\"a\"), Times.Once);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.VisualStudio.TestPlatform.ObjectModel;\nusing Newtonsoft.Json;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.DependencyInjection;\r\n\r\npublic sealed class ConfigurationBuilderExtensionsTests : FileUnitTest\r\n{\r\n    private IConfigurationRoot _configuration;\r\n    private IConfigurationRoot _configRoot;\r\n    private FileConfiguration _globalConfig;\r\n    private FileConfiguration _routeA;\r\n    private FileConfiguration _routeB;\r\n    private FileConfiguration _aggregate;\r\n    private FileConfiguration _envSpecific;\r\n    private FileConfiguration _combinedFileConfiguration;\r\n    private readonly Mock<IWebHostEnvironment> _hostingEnvironment;\n\r\n    public ConfigurationBuilderExtensionsTests()\r\n    {\r\n        _hostingEnvironment = new Mock<IWebHostEnvironment>();\n    }\n\n    protected override string EnvironmentName()\n        => _hostingEnvironment?.Object?.EnvironmentName ?? base.EnvironmentName();\r\n\n    [Fact]\r\n    public void Should_add_base_url_to_config()\r\n    {\n        // Arrange\r\n        _configuration = new ConfigurationBuilder()\r\n            .AddOcelotBaseUrl(\"test\")\n            .Build();\n\r\n        // Act\n        var actual = _configuration.GetValue(\"BaseUrl\", string.Empty);\r\n\n        // Assert\n        actual.ShouldBe(\"test\");\r\n    }\r\n\r\n    [Fact]\n    [Trait(\"PR\", \"1227\")]\n    [Trait(\"Issue\", \"1216\")]\n    public void Should_merge_files_to_file()\r\n    {\n        // Arrange\r\n        GivenTheEnvironmentIs(TestID);\n        GivenMultipleConfigurationFiles(TestID);\n\n        // Act\n        WhenIAddOcelotConfiguration(TestID);\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\n        TheOcelotPrimaryConfigFileExists(true);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_store_given_configurations_when_provided_file_configuration_object()\r\n    {\n        // Arrange\r\n        GivenTheEnvironmentIs(TestID);\n        GivenCombinedFileConfigurationObject();\n\n        // Act\n        WhenIAddOcelotConfigurationWithCombinedFileConfiguration();\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_merge_files_except_env()\r\n    {\r\n        // Arrange\r\n        GivenTheEnvironmentIs(TestID);\r\n        GivenMultipleConfigurationFiles(TestID, true);\r\n\n        // Act\n        WhenIAddOcelotConfiguration(TestID);\r\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\r\n        NotContainsEnvSpecificConfig();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_merge_files_in_specific_folder()\r\n    {\r\n        // Arrange\r\n        GivenMultipleConfigurationFiles(TestID);\n\n        // Act\n        WhenIAddOcelotConfiguration(TestID);\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\r\n    }\r\n\r\n    [Fact]\n    [Trait(\"PR\", \"1227\")]\n    [Trait(\"Issue\", \"1216\")]\n    public void Should_merge_files_to_memory()\r\n    {\n        // Arrange\r\n        GivenTheEnvironmentIs(TestID);\n        GivenMultipleConfigurationFiles(TestID);\r\n\n        // Act\n        WhenIAddOcelotConfiguration(TestID, MergeOcelotJson.ToMemory);\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\n        TheOcelotPrimaryConfigFileExists(false);\r\n    }\r\n\n    [Fact]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    public void Should_merge_files_with_null_environment()\r\n    {\n        // Arrange\r\n        environmentConfigFileName = null; // Ups!\n        const IWebHostEnvironment NullEnvironment = null; // Wow!\n        GivenMultipleConfigurationFiles(TestID, false);\r\n\n        // Act\n        _configRoot = new ConfigurationBuilder()\n            .AddOcelot(TestID, NullEnvironment, MergeOcelotJson.ToMemory, primaryConfigFileName, globalConfigFileName, environmentConfigFileName, false, false)\n            .Build();\r\n\n        // Assert\n        ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\n        TheOcelotPrimaryConfigFileExists(false);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"2084\")]\r\n    public void Should_use_relative_path_for_global_config()\r\n    {\r\n        // Arrange\r\n        GivenMultipleConfigurationFiles(TestID);\r\n\r\n        // Act\r\n        WhenIAddOcelotConfigurationWithDefaultFilePaths(TestID);\r\n\r\n        // Assert\r\n        var config = ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false);\r\n        config.ShouldNotBeNull().GlobalConfiguration.RequestIdKey.ShouldBe(nameof(Should_use_relative_path_for_global_config));\r\n    }\r\n\r\n    private void GivenCombinedFileConfigurationObject([CallerMemberName] string testName = null)\r\n    {\r\n        _combinedFileConfiguration = new FileConfiguration\r\n        {\r\n            GlobalConfiguration = GetFileGlobalConfigurationData(testName),\n            Routes = GetServiceARoutes().Concat(GetServiceBRoutes()).Concat(GetEnvironmentSpecificRoutes()).ToList(),\n            Aggregates = GetFileAggregatesRouteData(),\r\n        };\r\n    }\n\r\n    private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment = false, [CallerMemberName] string testName = null)\r\n    {\r\n        _globalConfig = new() { GlobalConfiguration = GetFileGlobalConfigurationData(testName) };\r\n        _routeA = new() { Routes = GetServiceARoutes() };\r\n        _routeB = new() { Routes = GetServiceBRoutes() };\r\n        _aggregate = new() { Aggregates = GetFileAggregatesRouteData() };\r\n        _envSpecific = new() { Routes = GetEnvironmentSpecificRoutes() };\n\n        var configParts = new Dictionary<string, FileConfiguration>\n        {\n            { \"global\", _globalConfig },\r\n            { \"routesA\", _routeA },\r\n            { \"routesB\", _routeB },\r\n            { \"aggregates\", _aggregate },\n        };\r\n\n        if (withEnvironment)\r\n        {\n            configParts.Add(EnvironmentName(), _envSpecific);\r\n        }\r\n\n        foreach (var part in configParts)\n        {\n            var filename = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, part.Key));\n            File.WriteAllText(filename, JsonConvert.SerializeObject(part.Value, Formatting.Indented));\n            files.Add(filename);\n        }\n    }\n\n    private static FileGlobalConfiguration GetFileGlobalConfigurationData(string requestIdKey = null) => new()\n    {\n        BaseUrl = \"BaseUrl\",\n        RateLimitOptions = new()\n        {\n            HttpStatusCode = 500,\n            ClientIdHeader = \"ClientIdHeader\",\n            QuotaExceededMessage = \"QuotaExceededMessage\",\n            RateLimitCounterPrefix = \"RateLimitCounterPrefix\",\n        },\n        ServiceDiscoveryProvider = new()\n        {\n            Scheme = \"https\",\n            Host = \"Host\",\n            Port = 80,\n            Type = \"Type\",\n        },\n        RequestIdKey = requestIdKey ?? \"RequestIdKey\",\n    };\n\n    private static List<FileAggregateRoute> GetFileAggregatesRouteData() => new()\n    {\r\n        new()\r\n        {\r\n            RouteKeys = [\"KeyB\", \"KeyBB\"],\r\n            UpstreamPathTemplate = \"UpstreamPathTemplate\",\r\n        },\r\n    };\n\n    private static FileRoute GetRoute(string suffix) => new()\n    {\n        DownstreamScheme = \"DownstreamScheme\" + suffix,\n        DownstreamPathTemplate = \"DownstreamPathTemplate\" + suffix,\n        Key = \"Key\" + suffix,\n        UpstreamHost = \"UpstreamHost\" + suffix,\n        UpstreamHttpMethod = [ \"UpstreamHttpMethod\" + suffix ],\n        DownstreamHostAndPorts = new()\n        {\n            new(\"Host\"+suffix, 80),\r\n        },\n    };\r\n\r\n    private static List<FileRoute> GetServiceARoutes() => new() { GetRoute(\"A\") };\n    private static List<FileRoute> GetServiceBRoutes() => new() { GetRoute(\"B\"), GetRoute(\"BB\") };\n    private static List<FileRoute> GetEnvironmentSpecificRoutes() => new() { GetRoute(\"Spec\") };\r\n\r\n    private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null)\r\n    {\n        _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(testName);\r\n        environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, testName));\r\n        files.Add(environmentConfigFileName);\n    }\r\n\r\n    private void WhenIAddOcelotConfigurationWithCombinedFileConfiguration()\r\n    {\r\n        _configRoot = new ConfigurationBuilder()\n            .AddOcelot(_combinedFileConfiguration, primaryConfigFileName, false, false)\n            .Build();\r\n    }\r\n\r\n    private void WhenIAddOcelotConfiguration(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile)\r\n    {\r\n        _configRoot = new ConfigurationBuilder()\n            .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, primaryConfigFileName, globalConfigFileName, environmentConfigFileName, false, false)\n            .Build();\r\n    }\r\n\r\n    private void WhenIAddOcelotConfigurationWithDefaultFilePaths(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile)\r\n    {\r\n        _configRoot = new ConfigurationBuilder()\r\n            .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, optional: false, reloadOnChange: false)\r\n            .Build();\r\n    }\r\n\r\n    private FileConfiguration ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(bool useCombinedConfig)\r\n    {\r\n        var fc = _configRoot.Get<FileConfiguration>();\r\n\n        fc.GlobalConfiguration.BaseUrl.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.BaseUrl : _globalConfig.GlobalConfiguration.BaseUrl);\r\n        fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.ClientIdHeader : _globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader);\r\n        fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders);\r\n        fc.GlobalConfiguration.RateLimitOptions.EnableHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.EnableHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.EnableHeaders);\r\n        fc.GlobalConfiguration.RateLimitOptions.HttpStatusCode.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.HttpStatusCode : _globalConfig.GlobalConfiguration.RateLimitOptions.HttpStatusCode);\r\n        fc.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage : _globalConfig.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage);\r\n        fc.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix : _globalConfig.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix);\r\n        fc.GlobalConfiguration.RequestIdKey.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RequestIdKey : _globalConfig.GlobalConfiguration.RequestIdKey);\r\n        fc.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Scheme : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Scheme);\r\n        fc.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Host : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Host);\r\n        fc.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Port : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Port);\r\n        fc.GlobalConfiguration.ServiceDiscoveryProvider.Type.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Type : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Type);\r\n\r\n        fc.Routes.Count.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.Routes.Count : _routeA.Routes.Count + _routeB.Routes.Count);\r\n\r\n        fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].DownstreamPathTemplate : _routeA.Routes[0].DownstreamPathTemplate));\r\n        fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].DownstreamPathTemplate : _routeB.Routes[0].DownstreamPathTemplate));\r\n        fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].DownstreamPathTemplate : _routeB.Routes[1].DownstreamPathTemplate));\r\n\r\n        fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].DownstreamScheme : _routeA.Routes[0].DownstreamScheme));\r\n        fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].DownstreamScheme : _routeB.Routes[0].DownstreamScheme));\r\n        fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].DownstreamScheme : _routeB.Routes[1].DownstreamScheme));\r\n\r\n        fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].Key : _routeA.Routes[0].Key));\r\n        fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].Key : _routeB.Routes[0].Key));\r\n        fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].Key : _routeB.Routes[1].Key));\r\n\r\n        fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].UpstreamHost : _routeA.Routes[0].UpstreamHost));\r\n        fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].UpstreamHost : _routeB.Routes[0].UpstreamHost));\r\n        fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].UpstreamHost : _routeB.Routes[1].UpstreamHost));\r\n\r\n        fc.Aggregates.Count.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.Aggregates.Count :_aggregate.Aggregates.Count);\r\n        return fc;\r\n    }\r\n\r\n    private void NotContainsEnvSpecificConfig()\r\n    {\r\n        var fc = _configRoot.Get<FileConfiguration>();\r\n        fc.Routes.ShouldNotContain(x => x.DownstreamScheme == _envSpecific.Routes[0].DownstreamScheme);\r\n        fc.Routes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.Routes[0].DownstreamPathTemplate);\r\n        fc.Routes.ShouldNotContain(x => x.Key == _envSpecific.Routes[0].Key);\r\n    }\r\n\r\n    private void TheOcelotPrimaryConfigFileExists(bool expected)\r\n        => File.Exists(primaryConfigFileName).ShouldBe(expected);\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs",
    "content": "using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.MiddlewareAnalysis;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ApplicationModels;\nusing Microsoft.AspNetCore.Mvc.ApplicationParts;\nusing Microsoft.AspNetCore.Mvc.Controllers;\nusing Microsoft.AspNetCore.Mvc.Infrastructure;\nusing Microsoft.AspNetCore.Mvc.Rendering;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Setter;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Infrastructure;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Multiplexer;\nusing Ocelot.Requester;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.UnitTests.Requester;\nusing Ocelot.Values;\nusing System.Reflection;\nusing System.Text.Encodings.Web;\nusing static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests;\n\nnamespace Ocelot.UnitTests.DependencyInjection;\n\npublic class OcelotBuilderTests : UnitTest\n{\n    private readonly IConfiguration _configRoot;\n    private readonly IServiceCollection _services;\n    private IServiceProvider _serviceProvider;\n    private IOcelotBuilder _ocelotBuilder;\n\n    public OcelotBuilderTests()\n    {\n        _configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());\n        _services = new ServiceCollection();\n        _services.AddSingleton(GetHostingEnvironment());\n        _services.AddSingleton(_configRoot);\n    }\n\n    private static IWebHostEnvironment GetHostingEnvironment()\n    {\n        var environment = new Mock<IWebHostEnvironment>();\n        environment.Setup(e => e.ApplicationName)\n            .Returns(typeof(OcelotBuilderTests).GetTypeInfo().Assembly.GetName().Name);\n        return environment.Object;\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"224\")] // https://github.com/ThreeMammals/Ocelot/pull/224\n    [Trait(\"Feat\", \"269\")] // https://github.com/ThreeMammals/Ocelot/pull/269\n    [Trait(\"Bug\", \"456\")] // https://github.com/ThreeMammals/Ocelot/pull/456\n    public void AddDelegatingHandler_Generic_NotGlobal()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddDelegatingHandler<FakeDelegatingHandler>();\n        _ocelotBuilder.AddDelegatingHandler<FakeDelegatingHandlerTwo>();\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsSpecificHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        ThenTheSpecificHandlersAreTransient();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"224\")] // https://github.com/ThreeMammals/Ocelot/pull/224\n    [Trait(\"Feat\", \"269\")] // https://github.com/ThreeMammals/Ocelot/pull/269\n    [Trait(\"Bug\", \"456\")] // https://github.com/ThreeMammals/Ocelot/pull/456\n    public void AddDelegatingHandler_Generic_Global()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddDelegatingHandler<FakeDelegatingHandler>(true);\n        _ocelotBuilder.AddDelegatingHandler<FakeDelegatingHandlerTwo>(true);\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        ThenTheGlobalHandlersAreTransient();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"943\")] // https://github.com/ThreeMammals/Ocelot/pull/943\n    public void AddDelegatingHandler_Type_TypeCheck()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        var ex = Assert.Throws<ArgumentOutOfRangeException>(\n            () => _ocelotBuilder.AddDelegatingHandler(typeof(OcelotBuilderTests))); // OcelotBuilderTests type is not DelegatingHandler one\n\n        // Assert\n        Assert.Equal(\"delegateType\", ex.ParamName);\n        Assert.Equal(nameof(OcelotBuilderTests), (string)ex.ActualValue);\n        Assert.Equal($\"It is not a delegating handler (Parameter 'delegateType'){Environment.NewLine}Actual value was OcelotBuilderTests.\", ex.Message);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"943\")] // https://github.com/ThreeMammals/Ocelot/pull/943\n    public void AddDelegatingHandler_Type_Global()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandler), true);\n        _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandlerTwo), true);\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        ThenTheGlobalHandlersAreTransient();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"943\")] // https://github.com/ThreeMammals/Ocelot/pull/943\n    public void AddDelegatingHandler_Type_NotGlobal()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandler));\n        _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandlerTwo));\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsSpecificHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        ThenTheSpecificHandlersAreTransient();\n    }\n\n    [Fact]\n    public void Should_set_up_services()\n    {\n        // Arrange, Act, Assert\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n    }\n\n    [Fact]\n    public void Should_return_ocelot_builder()\n    {\n        // Arrange, Act\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Assert\n        _ocelotBuilder.ShouldBeOfType<OcelotBuilder>();\n    }\n\n    [Fact]\n    public void Should_use_logger_factory()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n        _serviceProvider = _services.BuildServiceProvider(true);\n\n        // Act\n        var logger = _serviceProvider.GetService<IFileConfigurationSetter>();\n\n        // Assert\n        logger.ShouldNotBeNull();\n    }\n\n    [Fact]\n    public void Should_set_up_without_passing_in_config()\n    {\n        // Arrange, Act, Assert\n        _ocelotBuilder = _services.AddOcelot();\n    }\n\n    [Fact]\n    public void Should_add_singleton_defined_aggregators()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddSingletonDefinedAggregator<TestDefinedAggregator>();\n        _ocelotBuilder.AddSingletonDefinedAggregator<TestDefinedAggregator>();\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsSpecificAggregators<TestDefinedAggregator, TestDefinedAggregator>();\n\n        // Then The Aggregators Are Singleton<TestDefinedAggregator, TestDefinedAggregator>\n        var aggregators = _serviceProvider.GetServices<IDefinedAggregator>().ToList();\n        var first = aggregators[0];\n        aggregators = _serviceProvider.GetServices<IDefinedAggregator>().ToList();\n        var second = aggregators[0];\n        first.ShouldBe(second);\n    }\n\n    [Fact]\n    public void Should_add_transient_defined_aggregators()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddTransientDefinedAggregator<TestDefinedAggregator>();\n        _ocelotBuilder.AddTransientDefinedAggregator<TestDefinedAggregator>();\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsSpecificAggregators<TestDefinedAggregator, TestDefinedAggregator>();\n\n        // Then The Aggregators Are Transient<TestDefinedAggregator, TestDefinedAggregator>\n        var aggregators = _serviceProvider.GetServices<IDefinedAggregator>().ToList();\n        var first = aggregators[0];\n        aggregators = _serviceProvider.GetServices<IDefinedAggregator>().ToList();\n        var second = aggregators[0];\n        first.ShouldNotBe(second);\n    }\n\n    [Fact]\n    public void Should_add_custom_load_balancer_creators_by_default_ctor()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddCustomLoadBalancer<FakeCustomLoadBalancer>();\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators();\n    }\n\n    [Fact]\n    public void Should_add_custom_load_balancer_creators_by_factory_method()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddCustomLoadBalancer(() => new FakeCustomLoadBalancer());\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators();\n    }\n\n    [Fact]\n    public void Should_add_custom_load_balancer_creators_by_di_factory_method()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddCustomLoadBalancer(provider => new FakeCustomLoadBalancer());\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators();\n    }\n\n    [Fact]\n    public void Should_add_custom_load_balancer_creators_by_factory_method_with_arguments()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddCustomLoadBalancer((route, discoveryProvider) => new FakeCustomLoadBalancer());\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators();\n    }\n\n    [Fact]\n    public void Should_replace_iplaceholder()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddConfigPlaceholders();\n\n        // Assert\n        _serviceProvider = _services.BuildServiceProvider(true);\n        var placeholders = _serviceProvider.GetService<IPlaceholders>();\n        placeholders.ShouldBeOfType<ConfigAwarePlaceholders>();\n    }\n\n    [Fact]\n    public void Should_add_custom_load_balancer_creators()\n    {\n        // Arrange\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddCustomLoadBalancer((provider, route, discoveryProvider) => new FakeCustomLoadBalancer());\n\n        // Assert\n        ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators();\n    }\n\n    [Fact]\n    public void Should_use_default_mvc_builder()\n    {\n        // Arrange, Act\n        _ocelotBuilder = _services.AddOcelot();\n\n        // Assert\n        CstorShouldUseDefaultBuilderToInitMvcCoreBuilder();\n    }\n\n    private void CstorShouldUseDefaultBuilderToInitMvcCoreBuilder()\n    {\n        _ocelotBuilder.ShouldNotBeNull();\n        _ocelotBuilder.MvcCoreBuilder.ShouldNotBeNull();\n        _serviceProvider = _services.BuildServiceProvider(true);\n        using IServiceScope scope = _serviceProvider.CreateScope();\n\n        // .AddMvcCore()\n        _serviceProvider.GetServices<IConfigureOptions<MvcOptions>>()\n            .FirstOrDefault(s => s.GetType().Name == \"MvcCoreMvcOptionsSetup\")\n            .ShouldNotBeNull();\n\n        // .AddLogging()\n        _serviceProvider.GetService<ILoggerFactory>()\n            .ShouldNotBeNull().ShouldBeOfType<LoggerFactory>();\n        _serviceProvider.GetService<IConfigureOptions<LoggerFilterOptions>>()\n            .ShouldNotBeNull();\n\n        // .AddMiddlewareAnalysis()\n        _serviceProvider.GetService<IStartupFilter>()\n            .ShouldNotBeNull().ShouldBeOfType<AnalysisStartupFilter>();\n\n        // .AddWebEncoders()\n        _serviceProvider.GetService<HtmlEncoder>().ShouldNotBeNull();\n        _serviceProvider.GetService<JavaScriptEncoder>().ShouldNotBeNull();\n        _serviceProvider.GetService<UrlEncoder>().ShouldNotBeNull();\n\n        // .AddApplicationPart(assembly)\n        IList<ApplicationPart> list = _ocelotBuilder.MvcCoreBuilder.PartManager.ApplicationParts;\n        list.ShouldNotBeNull().Count.ShouldBe(2);\n        list.ShouldContain(part => part.Name == \"Ocelot\");\n        list.ShouldContain(part => part.Name == \"Ocelot.UnitTests\");\n\n        // .AddControllersAsServices()\n        _serviceProvider.GetService<IControllerActivator>()\n            .ShouldNotBeNull().ShouldBeOfType<ServiceBasedControllerActivator>();\n\n        // .AddAuthorization() -> .AddAuthorizationCore()\n        //_serviceProvider.GetService<Microsoft.AspNetCore.Authorization.AuthorizationMetrics>()\n        //    .ShouldNotBeNull().ShouldBeOfType<AuthorizationMetrics>();\n        _serviceProvider.GetService<IAuthorizationService>().ShouldNotBeNull()\n#if NET10_0_OR_GREATER\n            .GetType().Name.ShouldBe(\"DefaultAuthorizationServiceImpl\");\n#else\n            .ShouldBeOfType<DefaultAuthorizationService>();\n#endif\n        _serviceProvider.GetService<IAuthorizationPolicyProvider>()\n            .ShouldNotBeNull().ShouldBeOfType<DefaultAuthorizationPolicyProvider>();\n        _serviceProvider.GetService<IAuthorizationHandlerProvider>()\n            .ShouldNotBeNull().ShouldBeOfType<DefaultAuthorizationHandlerProvider>();\n        _serviceProvider.GetService<IAuthorizationEvaluator>()\n            .ShouldNotBeNull().ShouldBeOfType<DefaultAuthorizationEvaluator>();\n        _serviceProvider.GetService<IAuthorizationHandlerContextFactory>()\n            .ShouldNotBeNull().ShouldBeOfType<DefaultAuthorizationHandlerContextFactory>();\n        _serviceProvider.GetService<IAuthorizationHandler>()\n            .ShouldNotBeNull().ShouldBeOfType<PassThroughAuthorizationHandler>();\n\n        scope.ServiceProvider.GetService<IAuthenticationService>().ShouldNotBeNull()\n#if NET10_0_OR_GREATER\n            .GetType().Name.ShouldBe(\"AuthenticationServiceImpl\");\n#else\n            .ShouldBeOfType<AuthenticationService>();\n#endif\n        _serviceProvider.GetService<IApplicationModelProvider>()\n            .ShouldNotBeNull()\n            .GetType().Name.ShouldBe(\"AuthorizationApplicationModelProvider\");\n\n        // .AddNewtonsoftJson()\n        _serviceProvider.GetServices<IConfigureOptions<MvcOptions>>()\n            .FirstOrDefault(s => s.GetType().Name == \"NewtonsoftJsonMvcOptionsSetup\")\n            .ShouldNotBeNull();\n        _serviceProvider.GetService<IActionResultExecutor<JsonResult>>()\n            .ShouldNotBeNull()\n            .GetType().Name.ShouldBe(\"NewtonsoftJsonResultExecutor\");\n        _serviceProvider.GetService<IJsonHelper>()\n            .ShouldNotBeNull()\n            .GetType().Name.ShouldBe(\"NewtonsoftJsonHelper\");\n    }\n\n    [Fact]\n    public void Should_use_custom_mvc_builder_no_configuration()\n    {\n        // Arrange, Act\n        WhenISetupOcelotServicesWithCustomMvcBuider();\n\n        // Assert\n        CstorShouldUseCustomBuilderToInitMvcCoreBuilder();\n        ShouldFindConfiguration();\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Should_use_custom_mvc_builder_with_configuration(bool hasConfig)\n    {\n        // Arrange, Act\n        WhenISetupOcelotServicesWithCustomMvcBuider(\n            hasConfig ? _configRoot : null,\n            true);\n\n        // Assert\n        CstorShouldUseCustomBuilderToInitMvcCoreBuilder();\n        ShouldFindConfiguration();\n    }\n\n    [Fact]\n    public void CreateInstance_CreatedFromImplementationInstance()\n    {\n        // Arrange\n        var method = typeof(OcelotBuilder).GetMethod(\"CreateInstance\", BindingFlags.NonPublic | BindingFlags.Static);\n        ServiceDescriptor descriptor = new(GetType(), this);\n\n        // Act\n        var result = method.Invoke(null, [null, descriptor]);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<OcelotBuilderTests>(result);\n        Assert.Equal(this, result);\n    }\n\n    [Fact]\n    public void CreateInstance_CreatedByImplementationFactory()\n    {\n        // Arrange\n        var method = typeof(OcelotBuilder).GetMethod(\"CreateInstance\", BindingFlags.NonPublic | BindingFlags.Static);\n\n        object factory(IServiceProvider p) => this;\n        ServiceDescriptor descriptor = new(GetType(), factory, ServiceLifetime.Singleton);\n\n        // Act\n        var result = method.Invoke(null, [null, descriptor]);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<OcelotBuilderTests>(result);\n        Assert.Equal(this, result);\n    }\n\n    private bool _fakeCustomBuilderCalled;\n    private IMvcCoreBuilder FakeCustomBuilder(IMvcCoreBuilder builder, Assembly assembly)\n    {\n        _fakeCustomBuilderCalled = true;\n        return builder.AddJsonOptions(options =>\n            {\n                options.JsonSerializerOptions.WriteIndented = true;\n            });\n    }\n\n    private void WhenISetupOcelotServicesWithCustomMvcBuider(IConfiguration configuration = null, bool useConfigParam = false)\n    {\n        _fakeCustomBuilderCalled = false;\n        _ocelotBuilder = !useConfigParam\n            ? _services.AddOcelotUsingBuilder(FakeCustomBuilder)\n            : _services.AddOcelotUsingBuilder(configuration, FakeCustomBuilder);\n    }\n\n    private void CstorShouldUseCustomBuilderToInitMvcCoreBuilder()\n    {\n        _fakeCustomBuilderCalled.ShouldBeTrue();\n\n        _ocelotBuilder.ShouldNotBeNull();\n        _ocelotBuilder.MvcCoreBuilder.ShouldNotBeNull();\n        _serviceProvider = _services.BuildServiceProvider(true);\n\n        // .AddMvcCore()\n        _serviceProvider.GetServices<IConfigureOptions<MvcOptions>>()\n            .FirstOrDefault(s => s.GetType().Name == \"MvcCoreMvcOptionsSetup\")\n            .ShouldNotBeNull();\n\n        // .AddJsonOptions(options => { })\n        _serviceProvider.GetService<IOptionsMonitorCache<JsonOptions>>()\n            .ShouldNotBeNull().ShouldBeOfType<OptionsCache<JsonOptions>>();\n        _serviceProvider.GetService<IConfigureOptions<JsonOptions>>()\n            .ShouldNotBeNull().ShouldBeOfType<ConfigureNamedOptions<JsonOptions>>();\n    }\n\n    private void ShouldFindConfiguration()\n    {\n        _ocelotBuilder.ShouldNotBeNull();\n        var actual = _ocelotBuilder.Configuration.ShouldNotBeNull();\n        actual.Equals(_configRoot).ShouldBeTrue(); // check references equality\n        actual.ShouldBe(_configRoot);\n    }\n\n    private void ThenTheSpecificHandlersAreTransient()\n    {\n        var handlers = _serviceProvider.GetServices<DelegatingHandler>().ToList();\n        var first = handlers[0];\n        handlers = _serviceProvider.GetServices<DelegatingHandler>().ToList();\n        var second = handlers[0];\n        first.ShouldNotBe(second);\n    }\n\n    private void ThenTheGlobalHandlersAreTransient()\n    {\n        var handlers = _serviceProvider.GetServices<GlobalDelegatingHandler>().ToList();\n        var first = handlers[0].DelegatingHandler;\n        handlers = _serviceProvider.GetServices<GlobalDelegatingHandler>().ToList();\n        var second = handlers[0].DelegatingHandler;\n        first.ShouldNotBe(second);\n    }\n\n    private void ThenTheProviderIsRegisteredAndReturnsHandlers<TOne, TWo>()\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n        var handlers = _serviceProvider.GetServices<GlobalDelegatingHandler>().ToList();\n        handlers[0].DelegatingHandler.ShouldBeOfType<TOne>();\n        handlers[1].DelegatingHandler.ShouldBeOfType<TWo>();\n    }\n\n    private void ThenTheProviderIsRegisteredAndReturnsSpecificHandlers<TOne, TWo>()\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n        var handlers = _serviceProvider.GetServices<DelegatingHandler>().ToList();\n        handlers[0].ShouldBeOfType<TOne>();\n        handlers[1].ShouldBeOfType<TWo>();\n    }\n\n    private void ThenTheProviderIsRegisteredAndReturnsSpecificAggregators<TOne, TWo>()\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n        var handlers = _serviceProvider.GetServices<IDefinedAggregator>().ToList();\n        handlers[0].ShouldBeOfType<TOne>();\n        handlers[1].ShouldBeOfType<TWo>();\n    }\n\n    private void ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n        var creators = _serviceProvider.GetServices<ILoadBalancerCreator>().ToList();\n        creators.Count(c => c.GetType() == typeof(NoLoadBalancerCreator)).ShouldBe(1);\n        creators.Count(c => c.GetType() == typeof(RoundRobinCreator)).ShouldBe(1);\n        creators.Count(c => c.GetType() == typeof(CookieStickySessionsCreator)).ShouldBe(1);\n        creators.Count(c => c.GetType() == typeof(LeastConnectionCreator)).ShouldBe(1);\n        creators.Count(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator<FakeCustomLoadBalancer>)).ShouldBe(1);\n\n        // Call Create\n        var creator = creators.Single(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator<FakeCustomLoadBalancer>));\n        Assert.NotNull(creator);\n        var route = new DownstreamRouteBuilder().Build();\n        var provider = _serviceProvider.GetService<IServiceDiscoveryProvider>();\n        var response = creator.Create(route, provider);\n        Assert.NotNull(response);\n        Assert.False(response.IsError);\n        Assert.NotNull(response.Data);\n        Assert.IsType<FakeCustomLoadBalancer>(response.Data);\n    }\n\n    private class FakeCustomLoadBalancer : ILoadBalancer\n    {\n        public string Type => nameof(FakeCustomLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Ocelot.DependencyInjection;\nusing System.Reflection;\nusing Extensions = Ocelot.DependencyInjection.ServiceCollectionExtensions;\n\nnamespace Ocelot.UnitTests.DependencyInjection;\n\npublic class ServiceCollectionExtensionsTests\n{\n    [Fact]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    public void AddOcelot_NoConfiguration_DefaultConfiguration()\n    {\n        // Arrange\n        var services = new ServiceCollection();\n\n        // Act\n        var ocelot = services.AddOcelot();\n\n        // Assert\n        ocelot.ShouldNotBeNull()\n            .Configuration.ShouldNotBeNull();\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void FindConfiguration_HasDescriptor_HappyPath(bool hasConfig)\n    {\n        // Arrange\n        IConfiguration config = hasConfig ? new ConfigurationBuilder().Build() : null;\n        var descriptor = new ServiceDescriptor(typeof(IConfiguration), (p) => config, ServiceLifetime.Transient);\n        var services = new ServiceCollection().Add(descriptor);\n        IWebHostEnvironment env = null;\n\n        // Act\n        var method = typeof(Extensions).GetMethod(\"FindConfiguration\", BindingFlags.NonPublic | BindingFlags.Static);\n        var actual = (IConfiguration)method.Invoke(null, new object[] { services, env });\n\n        // Assert\n        actual.ShouldNotBeNull();\n        if (hasConfig)\n        {\n            actual.Equals(config).ShouldBeTrue();\n        }\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    public void AddOcelotUsingBuilder_NoConfigurationParam_ShouldFindConfiguration()\n    {\n        // Arrange\n        var services = new ServiceCollection();\n        var config = new ConfigurationBuilder().Build();\n        services.AddSingleton<IConfiguration>(config);\n\n        // Act\n        var ocelot = services.AddOcelotUsingBuilder(null, CustomBuilder);\n\n        // Assert\n        AssertConfiguration(ocelot, config);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"1986\")]\n    [Trait(\"Issue\", \"1518\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void AddOcelotUsingBuilder_WithConfigurationParam_ShouldFindConfiguration(bool shouldFind)\n    {\n        // Arrange\n        var services = new ServiceCollection();\n        var config = new ConfigurationBuilder().Build();\n        if (shouldFind)\n        {\n            services.AddSingleton<IConfiguration>(config);\n        }\n\n        // Act\n        var ocelot = services.AddOcelotUsingBuilder(shouldFind ? null : config, CustomBuilder);\n\n        // Assert\n        AssertConfiguration(ocelot, config);\n    }\n\n    private void AssertConfiguration(IOcelotBuilder ocelot, IConfiguration config)\n    {\n        ocelot.ShouldNotBeNull();\n        var actual = ocelot.Configuration.ShouldNotBeNull();\n        actual.Equals(config).ShouldBeTrue(); // check references equality\n        actual.ShouldBe(config);\n        Assert.Equal(1, _count);\n    }\n\n    private int _count;\n\n    private IMvcCoreBuilder CustomBuilder(IMvcCoreBuilder builder, Assembly assembly)\n    {\n        _count++;\n        return builder;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.PathManipulation;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\nusing Ocelot.Values;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.DownstreamPathManipulation;\n\n[Trait(\"Feat\", \"968\")] // https://github.com/ThreeMammals/Ocelot/pull/968\n[Trait(\"Release\", \"13.8.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0\npublic class ChangeDownstreamPathTemplateTests : UnitTest\n{\n    private readonly ChangeDownstreamPathTemplate _changeDownstreamPath;\n    private DownstreamPathTemplate _downstreamPathTemplate;\n    private readonly Mock<IClaimsParser> _parser;\n    private List<ClaimToThing> _configuration;\n    private List<Claim> _claims;\n    private Response _result;\n    private Response<string> _claimValue;\n    private List<PlaceholderNameAndValue> _placeholderValues;\n\n    public ChangeDownstreamPathTemplateTests()\n    {\n        _parser = new Mock<IClaimsParser>();\n        _changeDownstreamPath = new ChangeDownstreamPathTemplate(_parser.Object);\n    }\n\n    [Fact]\n    public void Should_change_downstream_path_request()\n    {\n        // Arrange\n        _claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        _placeholderValues = new List<PlaceholderNameAndValue>();\n        _configuration = new List<ClaimToThing>\n        {\n            new(\"path-key\", string.Empty, string.Empty, 0),\n        };\n        _downstreamPathTemplate = new DownstreamPathTemplate(\"/api/test/{path-key}\");\n        GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        WhenIChangeDownstreamPath();\n\n        // Assert\n        _result.IsError.ShouldBeFalse();\n        ThenClaimDataIsContainedInPlaceHolder(\"{path-key}\", \"value\");\n    }\n\n    [Fact]\n    public void Should_replace_existing_placeholder_value()\n    {\n        // Arrange\n        _claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        _placeholderValues = new List<PlaceholderNameAndValue>\n        {\n            new(\"{path-key}\", \"old_value\"),\n        };\n        _configuration = new List<ClaimToThing>\n        {\n            new(\"path-key\", string.Empty, string.Empty, 0),\n        };\n        _downstreamPathTemplate = new DownstreamPathTemplate(\"/api/test/{path-key}\");\n        GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        WhenIChangeDownstreamPath();\n\n        // Assert\n        _result.IsError.ShouldBeFalse();\n        ThenClaimDataIsContainedInPlaceHolder(\"{path-key}\", \"value\");\n    }\n\n    [Fact]\n    public void Should_return_error_when_no_placeholder_in_downstream_path()\n    {\n        // Arrange\n        _claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        _placeholderValues = new List<PlaceholderNameAndValue>();\n        _configuration = new List<ClaimToThing>\n        {\n            new(\"path-key\", string.Empty, string.Empty, 0),\n        };\n        _downstreamPathTemplate = new DownstreamPathTemplate(\"/api/test\");\n        GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        WhenIChangeDownstreamPath();\n\n        // Assert\n        _result.IsError.ShouldBe(true);\n        _result.Errors.Count.ShouldBe(1);\n        _result.Errors.First().ShouldBeOfType<CouldNotFindPlaceholderError>();\n    }\n\n    [Fact]\n    public void Should_return_error_when_claim_parser_returns_error()\n    {\n        // Arrange\n        _claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        _placeholderValues = new List<PlaceholderNameAndValue>();\n        _configuration = new List<ClaimToThing>\n        {\n            new(\"path-key\", string.Empty, string.Empty, 0),\n        };\n        _downstreamPathTemplate = new DownstreamPathTemplate(\"/api/test/{path-key}\");\n        GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error>\n            {\n               new AnyError(),\n            }));\n\n        // Act\n        WhenIChangeDownstreamPath();\n\n        // Assert\n        _result.IsError.ShouldBe(true);\n    }\n\n    private void GivenTheClaimParserReturns(Response<string> claimValue)\n    {\n        _claimValue = claimValue;\n        _parser.Setup(x => x.GetValue(It.IsAny<IEnumerable<Claim>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))\n            .Returns(_claimValue);\n    }\n\n    private void WhenIChangeDownstreamPath()\n        => _result = _changeDownstreamPath.ChangeDownstreamPath(_configuration, _claims, _downstreamPathTemplate, _placeholderValues);\n\n    private void ThenClaimDataIsContainedInPlaceHolder(string name, string value)\n    {\n        var placeHolder = _placeholderValues.FirstOrDefault(ph => ph.Name == name && ph.Value == value);\n        placeHolder.ShouldNotBeNull();\n        _placeholderValues.Count.ShouldBe(1);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamPathManipulation.Middleware;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.PathManipulation;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.DownstreamPathManipulation;\n\n[Trait(\"Feat\", \"968\")] // https://github.com/ThreeMammals/Ocelot/pull/968\n[Trait(\"Release\", \"13.8.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0\npublic class ClaimsToDownstreamPathMiddlewareTests : UnitTest\n{\n    private readonly Mock<IChangeDownstreamPathTemplate> _changePath;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ClaimsToDownstreamPathMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ClaimsToDownstreamPathMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ClaimsToDownstreamPathMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _changePath = new Mock<IChangeDownstreamPathTemplate>();\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\n        _middleware = new ClaimsToDownstreamPathMiddleware(_next, _loggerFactory.Object, _changePath.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_add_queries_correctly()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithClaimsToDownstreamPath(new List<ClaimToThing>\n                    {\n                        new(\"UserId\", \"Subject\", string.Empty, 0),\n                    })\n                    .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n                    .Build();\n        var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        // Arrange: Given The Down Stream Route Is\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n\n        // Arrange: Given The Change Downstream Path Returns Ok\n        _changePath.Setup(x => x.ChangeDownstreamPath(\n                It.IsAny<List<ClaimToThing>>(),\n                It.IsAny<IEnumerable<Claim>>(),\n                It.IsAny<DownstreamPathTemplate>(),\n                It.IsAny<List<PlaceholderNameAndValue>>()))\n            .Returns(new OkResponse());\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _changePath.Verify(x => x.ChangeDownstreamPath(\n                It.IsAny<List<ClaimToThing>>(),\n                It.IsAny<IEnumerable<Claim>>(),\n                _httpContext.Items.DownstreamRoute().DownstreamPathTemplate,\n                _httpContext.Items.TemplatePlaceholderNameAndValues()), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.Infrastructure.Extensions;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder;\n\npublic class DiscoveryDownstreamRouteFinderTests : UnitTest\n{\n    private readonly DiscoveryDownstreamRouteFinder _finder;\n    private QoSOptions _qoSOptions;\n    private LoadBalancerOptions _loadBalancerOptions;\n    private Response<Ocelot.DownstreamRouteFinder.DownstreamRouteHolder> _result;\n    private string _upstreamHost;\n    private string _upstreamUrlPath;\n    private string _upstreamHttpMethod;\n    private IHeaderDictionary _upstreamHeaders;\n    private IInternalConfiguration _configuration;\n    private Response<Ocelot.DownstreamRouteFinder.DownstreamRouteHolder> _resultTwo;\n    private readonly string _upstreamQuery;\n    private readonly Mock<IUpstreamHeaderTemplatePatternCreator> _upstreamHeaderTemplatePatternCreator = new();\n    private readonly HttpHandlerOptions _handlerOptions;\n    private readonly MetadataOptions _metadataOptions;\n    private readonly RateLimitOptions _rateLimitOptions;\n\n    public DiscoveryDownstreamRouteFinderTests()\n    {\n        _qoSOptions = new(new FileQoSOptions());\n        _handlerOptions = new();\n        _loadBalancerOptions = new(nameof(NoLoadBalancer), default, default);\n        _metadataOptions = new MetadataOptions();\n        _rateLimitOptions = new RateLimitOptions();\n        _finder = new(new RouteKeyCreator(), _upstreamHeaderTemplatePatternCreator.Object);\n        _upstreamQuery = string.Empty;\n    }\n\n    [Fact]\n    public void Should_create_downstream_route()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamRouteIsCreated();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"1229\")]\n    public void Should_create_downstream_route_with_rate_limit_options()\n    {\n        // Arrange\n        var rateLimitOptions = new RateLimitOptions()\n        {\n            EnableRateLimiting = true,\n            ClientIdHeader = \"test\",\n        };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithServiceName(\"auth\")\n            .WithRateLimitOptions(rateLimitOptions)\n            .WithLoadBalancerKey(\"|auth\")\n            .WithLoadBalancerOptions(_loadBalancerOptions)\n            .WithQosOptions(_qoSOptions)\n            .WithHttpHandlerOptions(_handlerOptions)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        var route = new Route(true, downstreamRoute); // create dynamic route\n        GivenInternalConfiguration(route);\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamRouteIsCreated(lbKey: downstreamRoute.LoadBalancerKey);\n\n        // Assert: With RateLimitOptions\n        var actual = _result.Data.Route.DownstreamRoute[0].RateLimitOptions;\n        actual.EnableRateLimiting.ShouldBeTrue();\n        actual.EnableRateLimiting.ShouldBe(rateLimitOptions.EnableRateLimiting);\n        actual.ClientIdHeader.ShouldBe(rateLimitOptions.ClientIdHeader);\n    }\n\n    [Fact]\n    public void Should_cache_downstream_route()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/geoffisthebest/\";\n\n        // Act\n        WhenICreate();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/geoffisthebest/\";\n        WhenICreateAgain();\n\n        // Assert\n        _result.ShouldBe(_resultTwo);\n    }\n\n    [Fact]\n    public void Should_not_cache_downstream_route()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/geoffistheworst/\";\n\n        // Act\n        WhenICreate();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/geoffisthebest/\";\n        WhenICreateAgain();\n\n        // Assert\n        _result.ShouldNotBe(_resultTwo);\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_with_no_path()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/auth/\";\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamPathIsForwardSlash();\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_with_only_first_segment_no_traling_slash()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/auth\";\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamPathIsForwardSlash();\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_with_segments_no_traling_slash()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        _upstreamUrlPath = \"/auth/test\";\n\n        // Act\n        WhenICreate();\n\n        // Assert: Then the path does not have trailing slash\n        var actual = _result.Data.Route.DownstreamRoute[0];\n        actual.DownstreamPathTemplate.Value.ShouldBe(\"/test\");\n        actual.ServiceName.ShouldBe(\"auth\");\n        actual.ServiceNamespace.ShouldBeEmpty();\n        actual.LoadBalancerKey.ShouldBe(\".auth\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"351\")]\n    [Trait(\"PR\", \"2324\")] // This PR resolves the issue of forwarding the query string to the downstream when service discovery (dynamic routing), fixing a bug in the QoS Key construction for caching within the ResiliencePipelineRegistry<T>. It now reuses the load balancing key to address the problem.\n    public void Should_create_downstream_route_and_forward_query_string()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n        const string queryString = \"?test=1&best=2\";\n        _upstreamUrlPath = \"/auth/test\" + queryString;\n\n        // Act\n        WhenICreate();\n\n        // Assert: Then the query string is removed\n        var actual = _result.Data.Route.DownstreamRoute[0];\n        actual.DownstreamPathTemplate.Value.ShouldContain(queryString); // !!!\n        actual.DownstreamPathTemplate.Value.ShouldBe(\"/test?test=1&best=2\");\n        actual.ServiceName.ShouldBe(\"auth\");\n        actual.ServiceNamespace.ShouldBeEmpty();\n        actual.LoadBalancerKey.ShouldBe(\".auth\");\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_for_sticky_sessions()\n    {\n        // Arrange\n        _loadBalancerOptions = new LoadBalancerOptions(nameof(CookieStickySessions), \"boom\", 1);\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        var actual = _result.Data.Route.DownstreamRoute[0];\n        actual.LoadBalancerKey.ShouldBe(\"CookieStickySessions:boom\");\n        actual.LoadBalancerOptions.Type.ShouldBe(\"CookieStickySessions\");\n        actual.LoadBalancerOptions.ShouldBe(_loadBalancerOptions);\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_with_qos()\n    {\n        // Arrange\n        _qoSOptions = new QoSOptions(1)\n        {\n            MinimumThroughput = 1,\n        };\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert: Then the Qos options are set\n        var actual = _result.Data.Route.DownstreamRoute[0];\n        actual.QosOptions.ShouldNotBeNull();\n        actual.QosOptions.UseQos.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_create_downstream_route_with_handler_options()\n    {\n        // Arrange\n        GivenInternalConfiguration();\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert: Then The Handler Options Are Set\n        _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2324\")]\n    [InlineData(\"/service1\", \"service1\", \"\")]\n    [InlineData(\"/service2/\", \"service2\", \"\")]\n    [InlineData(\"/service3/bla\", \"service3\", \"\")]\n    [InlineData(\"/namespace1.service1\", \"service1\", \"namespace1\")]\n    [InlineData(\"/namespace2.service2/\", \"service2\", \"namespace2\")]\n    [InlineData(\"/namespace3.service3/bla-bla\", \"service3\", \"namespace3\")]\n    [InlineData(\"/namespace4.service.4/ha-a\", \"service.4\", \"namespace4\")]\n    [InlineData(\"/name.space5.service5/ha-ha\", \"space5.service5\", \"name\")]\n    public void GetServiceName(string urlPath, string expected, string expectedNamespace)\n    {\n        var method = _finder.GetType().GetMethod(nameof(GetServiceName), BindingFlags.Instance | BindingFlags.NonPublic);\n        object[] parameters = [urlPath, null];\n\n        // Act\n        string actual = (string)method.Invoke(_finder, parameters);\n        string actualNamespace = (string)parameters[1];\n\n        Assert.Equal(expected, actual);\n        Assert.Equal(expectedNamespace, actualNamespace);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    public void Should_create_downstream_route_with_load_balancer_options()\n    {\n        // Arrange\n        var lbOptions = new LoadBalancerOptions(\"testBalancer\", \"testKey\", 3);\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithServiceName(\"auth\")\n            .WithLoadBalancerOptions(lbOptions)\n            .WithLoadBalancerKey(\"|auth\")\n            .WithMetadata(_metadataOptions)\n            .WithRateLimitOptions(_rateLimitOptions)\n            .WithQosOptions(_qoSOptions)\n            .WithHttpHandlerOptions(_handlerOptions)\n            .WithDownstreamScheme(\"http\")\n            .Build();\n        var route = new Route(true, downstreamRoute); // create dynamic route\n        GivenInternalConfiguration(route);\n        GivenTheConfiguration();\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamRouteIsCreated(lbType: \"testBalancer\", lbKey: \"|auth\");\n        var downstream = _result.Data.Route.DownstreamRoute[0];\n        downstream.LoadBalancerOptions.ShouldNotBeNull();\n        downstream.LoadBalancerOptions.Type.ShouldBe(\"testBalancer\");\n        downstream.LoadBalancerOptions.Key.ShouldBe(\"testKey\");\n        downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"2319\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\n    [InlineData(false)]\n    [InlineData(true)]\n    public void ShouldFindFirstOrDefaultDownstreamRoute_WithOrWithoutServiceNamespace(bool hasNamespace)\n    {\n        // Arrange\n        var lbOptions = new LoadBalancerOptions(\"testBalancer\", \"testKey\", 3);\n        var dRoute1 = new DownstreamRouteBuilder()\n            .WithServiceName(\"service1\")\n            .Build();\n        var dRoute2 = new DownstreamRouteBuilder()\n            .WithServiceName(\"service2\")\n            .WithServiceNamespace(hasNamespace ? \"namespace2\" : string.Empty)\n            .WithLoadBalancerKey(\"namespace2-service2\")\n            .WithLoadBalancerOptions(lbOptions)\n            .WithQosOptions(_qoSOptions)\n            .WithHttpHandlerOptions(_handlerOptions)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        var route = new Route(true)\n        {\n            DownstreamRoute = [dRoute1, dRoute2],\n        };\n        GivenInternalConfiguration(route, 1);\n        GivenTheConfiguration();\n        _upstreamUrlPath = hasNamespace\n            ? $\"/{dRoute2.ServiceNamespace}.{dRoute2.ServiceName}/test\"\n            : $\"/{dRoute2.ServiceName}/test\";\n\n        // Act\n        WhenICreate();\n\n        // Assert\n        ThenTheDownstreamRouteIsCreated(\"service2\", hasNamespace ? \"namespace2\" : \"\", \"testBalancer\", \"namespace2-service2\");\n        var downstream = _result.Data.Route.DownstreamRoute[0];\n        downstream.LoadBalancerOptions.ShouldNotBeNull();\n        downstream.LoadBalancerOptions.Type.ShouldBe(\"testBalancer\");\n        downstream.LoadBalancerOptions.Key.ShouldBe(\"testKey\");\n        downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3);\n    }\n\n    private void ThenTheDownstreamRouteIsCreated(string serviceName = null, string serviceNamespace = null, string lbType = null, string lbKey = null)\n    {\n        _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(\"/test\");\n        _result.Data.Route.UpstreamHttpMethod.ShouldContain(HttpMethod.Get);\n        _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe(serviceName ?? \"auth\");\n        _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBe(serviceNamespace ?? string.Empty);\n        _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(lbKey ?? \".auth\");\n        _result.Data.Route.DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue();\n        _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldNotBeNull();\n        _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull();\n        _result.Data.Route.DownstreamRoute[0].DownstreamScheme.ShouldBe(\"http\");\n        _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(lbType ?? nameof(NoLoadBalancer));\n        _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions);\n        _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull();\n        _result.Data.Route.UpstreamTemplatePattern.ShouldNotBeNull();\n        _result.Data.Route.DownstreamRoute[0].UpstreamPathTemplate.ShouldNotBeNull();\n        var kv = _upstreamHeaders.First();\n        _result.Data.Route.UpstreamHeaderTemplates.ShouldNotBeNull()\n            .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value);\n        _result.Data.Route.DownstreamRoute[0].UpstreamHeaders.ShouldNotBeNull()\n            .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value);\n    }\n\n    private void ThenTheDownstreamPathIsForwardSlash()\n    {\n        _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(\"/\");\n        _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe(\"auth\");\n        _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBeEmpty();\n        _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(\".auth\");\n    }\n\n    private void GivenTheConfiguration()\n    {\n        _upstreamHost = \"doesnt matter\";\n        _upstreamUrlPath = \"/auth/test\";\n        _upstreamHttpMethod = \"GET\";\n        _upstreamHeaders = new HeaderDictionary()\n        {\n            { \"testHeader\", \"testHeaderValue\" },\n        };\n        var kv = _upstreamHeaders.First();\n        _upstreamHeaderTemplatePatternCreator.Setup(x => x.Create(It.IsAny<IHeaderDictionary>(), It.IsAny<bool>()))\n            .Returns(new Dictionary<string, UpstreamHeaderTemplate>()\n            {\n                { kv.Key, new(kv.Value, kv.Value) },\n            });\n    }\n\n    private void GivenInternalConfiguration(Route route = null, int index = 0)\n    {\n        var dr = route?.DownstreamRoute[index];\n        _configuration = new InternalConfiguration(route is null ? null : [route])\n        {\n            AdministrationPath = \"/AdminPath\",\n            RequestId = \"requestID\",\n            LoadBalancerOptions = dr?.LoadBalancerOptions ?? _loadBalancerOptions,\n            DownstreamScheme = (dr?.DownstreamScheme).IfEmpty(Uri.UriSchemeHttp),\n            QoSOptions = dr?.QosOptions ?? _qoSOptions,\n            HttpHandlerOptions = dr?.HttpHandlerOptions ?? _handlerOptions,\n            DownstreamHttpVersion = dr?.DownstreamHttpVersion ?? new Version(\"1.1\"),\n            DownstreamHttpVersionPolicy = dr?.DownstreamHttpVersionPolicy ?? HttpVersionPolicy.RequestVersionOrLower,\n            MetadataOptions = dr?.MetadataOptions ?? _metadataOptions,\n            RateLimitOptions = dr?.RateLimitOptions ?? _rateLimitOptions,\n            Timeout = dr?.Timeout ?? 111,\n        };\n    }\n\n    private void WhenICreate()\n    {\n        _result = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders);\n    }\n\n    private void WhenICreateAgain()\n    {\n        _resultTwo = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.DownstreamRouteFinder.Middleware;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder;\n\npublic class DownstreamRouteFinderMiddlewareTests : UnitTest\n{\n    private readonly Mock<IDownstreamRouteProvider> _finder;\n    private readonly Mock<IDownstreamRouteProviderFactory> _factory;\n    private Response<DownstreamRouteHolder> _downstreamRoute;\n    private IInternalConfiguration _config;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly DownstreamRouteFinderMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public DownstreamRouteFinderMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _finder = new Mock<IDownstreamRouteProvider>();\n        _factory = new Mock<IDownstreamRouteProviderFactory>();\n        _factory.Setup(x => x.Get(It.IsAny<IInternalConfiguration>())).Returns(_finder.Object);\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<DownstreamRouteFinderMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _factory.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_scoped_data_repository_correctly()\n    {\n        // Arrange\n        var config = new InternalConfiguration();\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"any old string\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .Build();\n        GivenTheDownStreamRouteFinderReturns(new(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheFollowingConfig(config);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheScopedDataRepositoryIsCalledCorrectly();\n    }\n\n    private void GivenTheFollowingConfig(IInternalConfiguration config)\n    {\n        _config = config;\n        _httpContext.Items.SetIInternalConfiguration(config);\n    }\n\n    private void GivenTheDownStreamRouteFinderReturns(DownstreamRouteHolder downstreamRoute)\n    {\n        _downstreamRoute = new OkResponse<DownstreamRouteHolder>(downstreamRoute);\n        _finder\n            .Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IInternalConfiguration>(), It.IsAny<string>(), It.IsAny<IHeaderDictionary>()))\n            .Returns(_downstreamRoute);\n    }\n\n    private void ThenTheScopedDataRepositoryIsCalledCorrectly()\n    {\n        _httpContext.Items.TemplatePlaceholderNameAndValues().ShouldBe(_downstreamRoute.Data.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.IInternalConfiguration().ServiceProviderConfiguration.ShouldBe(_config.ServiceProviderConfiguration);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.Builder;\r\nusing Ocelot.DownstreamRouteFinder;\r\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\r\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\r\nusing Ocelot.Responses;\r\nusing Ocelot.Values;\nusing _DownstreamRouteFinder_ = Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder;\n\r\nnamespace Ocelot.UnitTests.DownstreamRouteFinder;\r\n\npublic class DownstreamRouteFinderTests : UnitTest\r\n{\r\n    private readonly _DownstreamRouteFinder_ _routeFinder;\r\n    private readonly Mock<IUrlPathToUrlTemplateMatcher> _mockUrlMatcher;\n    private readonly Mock<IHeadersToHeaderTemplatesMatcher> _mockHeadersMatcher;\r\n    private readonly Mock<IPlaceholderNameAndValueFinder> _urlPlaceholderFinder;\n    private readonly Mock<IHeaderPlaceholderNameAndValueFinder> _headerPlaceholderFinder;\r\n    private string _upstreamUrlPath;\r\n    private Response<DownstreamRouteHolder> _result;\r\n    private List<Route> _routesConfig;\r\n    private InternalConfiguration _config;\r\n    private UrlMatch _match;\r\n    private string _upstreamHttpMethod;\r\n    private string _upstreamHost;\n    private IHeaderDictionary _upstreamHeaders;\r\n    private string _upstreamQuery;\r\n\r\n    public DownstreamRouteFinderTests()\r\n    {\r\n        _mockUrlMatcher = new Mock<IUrlPathToUrlTemplateMatcher>();\n        _mockHeadersMatcher = new Mock<IHeadersToHeaderTemplatesMatcher>();\r\n        _urlPlaceholderFinder = new Mock<IPlaceholderNameAndValueFinder>();\n        _headerPlaceholderFinder = new Mock<IHeaderPlaceholderNameAndValueFinder>();\r\n        _routeFinder = new _DownstreamRouteFinder_(_mockUrlMatcher.Object, _urlPlaceholderFinder.Object, _mockHeadersMatcher.Object, _headerPlaceholderFinder.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_highest_priority_when_first()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        var expectedRoute = GivenRoute(method: \"Post\", priority: 1);\r\n        _routesConfig = new()\r\n        {\r\n            expectedRoute,\r\n            GivenRoute(method: \"Post\", priority: 0),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            expectedRoute));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_highest_priority_when_lowest()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        var expectedRoute = GivenRoute(method: \"Post\", priority: 1);\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(method: \"Post\", priority: 0),\r\n            expectedRoute,\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            expectedRoute));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n        ThenTheUrlMatcherIsCalledCorrectly();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_append_slash_to_upstream_url_path()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n\r\n        // Assert: Then The Url Matcher Is Called Correctly\r\n        _mockUrlMatcher.Verify(x => x.Match(\"matchInUrlMatcher\", _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_if_upstream_path_and_upstream_template_are_the_same()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_correct_route_for_http_verb()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(downstream: \"someDownstreamPath\", method: \"Get\", priority: 1),\r\n            GivenRoute(downstream: \"someDownstreamPathForAPost\", method: \"Post\", priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(downstream: \"someDownstreamPathForAPost\", method: \"Post\", priority: 1)\r\n        ));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_return_route()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"dontMatchPath/\";\n        _upstreamQuery = string.Empty;\r\n        _routesConfig = new List<Route>\r\n        {\r\n            GivenRoute(downstream: \"somPath\", upstream: \"somePath\", priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(false));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\r\n        ThenTheUrlMatcherIsCalledCorrectly();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_correct_route_for_http_verb_setting_multiple_upstream_http_method()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(upstreamMethods: [\"Get\", \"Post\"], priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(upstreamMethods: [\"Post\"], priority: 1)));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_correct_route_for_http_verb_setting_all_upstream_http_method()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(upstreamMethods: [], priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(upstreamMethods: [\"Post\"], priority: 1)));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_return_route_for_http_verb_not_setting_in_upstream_http_method()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"someUpstreamPath\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(upstreamMethods: [\"Get\", \"Patch\", \"Delete\"], priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Post\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\r\n        ThenTheUrlMatcherIsNotCalled();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_when_host_matches()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"MATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(host: \"MATCH\", priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n        ThenTheUrlMatcherIsCalledCorrectly();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_when_upstreamhost_is_null()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"MATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(host: null, priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n        ThenTheUrlMatcherIsCalledCorrectly();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_return_route_when_host_doesnt_match()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"DONTMATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(host: \"MATCH\", upstreamMethods: [\"Get\"], priority: 1),\r\n            GivenRoute(host: \"MATCH\", upstreamMethods: [], priority: 1), // empty list of methods\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\r\n        ThenTheUrlMatcherIsNotCalled();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_return_route_when_host_doesnt_match_with_empty_upstream_http_method()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"DONTMATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(host: \"MATCH\", upstreamMethods: [], priority: 1), // empty list of methods\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\r\n        ThenTheUrlMatcherIsNotCalled();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_when_host_does_match_with_empty_upstream_http_method()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"MATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(host: \"MATCH\", upstreamMethods: [], priority: 1), // empty list of methods\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheUrlMatcherIsCalledCorrectly(1, 0);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_route_when_host_matches_but_null_host_on_same_path_first()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHost = \"MATCH\";\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(downstream: \"THENULLPATH\", priority: 1),\r\n            GivenRoute(host: \"MATCH\", priority: 1), // empty list of methods\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n\r\n        // Assert\r\n        ThenTheFollowingIsReturned(new(\r\n            new List<PlaceholderNameAndValue>(),\r\n            GivenRoute(priority: 1)));\r\n        ThenTheUrlMatcherIsCalledCorrectly(1, 0);\r\n        ThenTheUrlMatcherIsCalledCorrectly(1, 1);\r\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public void Should_return_route_when_upstream_headers_match()\r\n    {\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"header1\"] = \"headerValue1\",\n            [\"header2\"] = \"headerValue2\",\n            [\"header3\"] = \"headerValue3\",\n        };\n        var upstreamHeadersConfig = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"header1\"] = new UpstreamHeaderTemplate(\"headerValue1\", \"headerValue1\"),\n            [\"header2\"] = new UpstreamHeaderTemplate(\"headerValue2\", \"headerValue2\"),\n        };\r\n        var urlPlaceholders = new List<PlaceholderNameAndValue> { new(\"url\", \"urlValue\") };\n        var headerPlaceholders = new List<PlaceholderNameAndValue> { new(\"header\", \"headerValue\") };\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHeaders = upstreamHeaders;\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(urlPlaceholders));\n        GivenTheHeaderPlaceholderAndNameFinderReturns(headerPlaceholders);\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(headers: upstreamHeadersConfig, priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\n        GivenTheHeadersMatcherReturns(true);\n        _upstreamHttpMethod = \"Get\";\n\n        // Act\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\n\n        // Assert\n        ThenTheFollowingIsReturned(new(\n            urlPlaceholders.Union(headerPlaceholders).ToList(),\r\n            GivenRoute(priority: 1)));\n        ThenTheUrlMatcherIsCalledCorrectly();\r\n    }\n\n    [Fact]\r\n    [Trait(\"PR\", \"1312\")]\r\n    [Trait(\"Feat\", \"360\")]\r\n    public void Should_not_return_route_when_upstream_headers_dont_match()\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\n        var upstreamHeadersConfig = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"header1\"] = new UpstreamHeaderTemplate(\"headerValue1\", \"headerValue1\"),\n            [\"header2\"] = new UpstreamHeaderTemplate(\"headerValue2\", \"headerValue2\"),\n        };\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        _upstreamHeaders = new HeaderDictionary() { { \"header1\", \"headerValue1\" } };\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new List<PlaceholderNameAndValue>()));\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(headers: upstreamHeadersConfig, priority: 1),\r\n            GivenRoute(headers: upstreamHeadersConfig, priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\n        GivenTheHeadersMatcherReturns(false);\n        _upstreamHttpMethod = \"Get\";\n\n        // Act\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\n\n        // Assert\n        _result.IsError.ShouldBeTrue();\r\n    }\r\n\r\n    [Theory]\r\n    [Trait(\"Feat\", \"585\")]\r\n    [Trait(\"Feat\", \"2319\")] // https://github.com/ThreeMammals/Ocelot/pull/2324\r\n    [InlineData(false)]\r\n    [InlineData(true)]\r\n    public void Should_filter_static_routes(bool isDynamic)\r\n    {\r\n        // Arrange\r\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build();\r\n        _upstreamUrlPath = \"matchInUrlMatcher/\";\n        _upstreamQuery = string.Empty;\r\n        GivenTheTemplateVariableAndNameFinderReturns(new OkResponse<List<PlaceholderNameAndValue>>(new()));\r\n        GivenTheHeaderPlaceholderAndNameFinderReturns(new List<PlaceholderNameAndValue>());\r\n        _routesConfig = new()\r\n        {\r\n            GivenRoute(priority: 1),\r\n            GivenRoute(isDynamic: isDynamic, priority: 1),\r\n        };\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        GivenTheUrlMatcherReturns(new UrlMatch(true));\r\n        GivenTheHeadersMatcherReturns(true);\r\n        _upstreamHttpMethod = \"Get\";\r\n\r\n        // Act, Assert\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n        _result.Data.Route.IsDynamic.ShouldBeFalse();\r\n\r\n        // Act, Assert 2\r\n        _routesConfig.RemoveAll(r => !r.IsDynamic); // remove all static routes\r\n        GivenTheConfigurationIs(string.Empty, serviceProviderConfig);\r\n        _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders);\r\n        _result.IsError.ShouldBeTrue();\r\n    }\r\n\r\n    private static Route GivenRoute(bool? isDynamic = null, string downstream = null,\r\n        List<string> upstreamMethods = null, string method = null,\r\n        UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null,\r\n        string host = null,\r\n        IDictionary<string, UpstreamHeaderTemplate> headers = null)\r\n    {\r\n        var route = GivenDownstreamRoute(downstream, upstreamMethods, method, upTemplate, upstream, priority);\r\n        upstream ??= \"someUpstreamPath\";\r\n        upTemplate ??= new(upstream, priority ?? 1, false, upstream);\r\n        upstreamMethods ??= [method ?? HttpMethods.Get];\r\n        return new(isDynamic ?? false)\r\n        {\r\n            DownstreamRoute = [route],\r\n            UpstreamHttpMethod = upstreamMethods.Select(m => new HttpMethod(m)).ToHashSet(),\r\n            UpstreamTemplatePattern = upTemplate,\r\n            UpstreamHost = host,\r\n            UpstreamHeaderTemplates = headers,\r\n        };\r\n    }\r\n\r\n    private static DownstreamRoute GivenDownstreamRoute(string downstream = null,\r\n        List<string> upstreamMethods = null, string method = null,\r\n        UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null)\r\n        => new DownstreamRouteBuilder()\r\n            .WithDownstreamPathTemplate(downstream ?? \"someDownstreamPath\")\r\n            .WithUpstreamHttpMethod(upstreamMethods ?? [method ?? HttpMethods.Get])\r\n            .WithUpstreamPathTemplate(upTemplate ?? new(upstream ?? \"someUpstreamPath\", priority ?? 1, false, upstream ?? \"someUpstreamPath\"))\r\n            .Build();\r\n\r\n    private void GivenTheTemplateVariableAndNameFinderReturns(Response<List<PlaceholderNameAndValue>> response)\r\n    {\r\n        _urlPlaceholderFinder\r\n            .Setup(x => x.Find(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))\r\n            .Returns(response);\r\n    }\r\n\r\n    private void GivenTheHeaderPlaceholderAndNameFinderReturns(List<PlaceholderNameAndValue> placeholders)\r\n    {\r\n        _headerPlaceholderFinder\r\n            .Setup(x => x.Find(It.IsAny<IHeaderDictionary>(), It.IsAny<Dictionary<string, UpstreamHeaderTemplate>>()))\r\n            .Returns(placeholders);\r\n    }\r\n\r\n    private void ThenTheUrlMatcherIsCalledCorrectly()\r\n    {\r\n        _mockUrlMatcher\r\n            .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once);\r\n    }\r\n\r\n    private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0)\r\n    {\r\n        _mockUrlMatcher\r\n            .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[index].UpstreamTemplatePattern), Times.Exactly(times));\r\n    }\r\n\r\n    private void ThenTheUrlMatcherIsNotCalled()\r\n    {\r\n        _mockUrlMatcher\r\n            .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Never);\r\n    }\r\n\r\n    private void GivenTheUrlMatcherReturns(UrlMatch match)\r\n    {\r\n        _match = match;\r\n        _mockUrlMatcher\r\n            .Setup(x => x.Match(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<UpstreamPathTemplate>()))\r\n            .Returns(_match);\r\n    }\n\n    private void GivenTheHeadersMatcherReturns(bool headersMatch)\n    {\n        _mockHeadersMatcher\n            .Setup(x => x.Match(It.IsAny<IHeaderDictionary>(), It.IsAny<Dictionary<string, UpstreamHeaderTemplate>>()))\n            .Returns(headersMatch);\n    }\r\n\r\n    private void GivenTheConfigurationIs(string adminPath, ServiceProviderConfiguration serviceProviderConfig)\r\n    {\r\n        _config = new InternalConfiguration(_routesConfig.ToArray())\r\n        {\r\n            AdministrationPath = adminPath,\r\n            DownstreamHttpVersion = new Version(\"1.1\"),\r\n            DownstreamHttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,\r\n            HttpHandlerOptions = new(),\r\n            LoadBalancerOptions = new(),\r\n            QoSOptions = new(),\r\n            ServiceProviderConfiguration = serviceProviderConfig,\r\n        };\r\n    }\r\n\r\n    private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected)\r\n    {\r\n        _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.Route.DownstreamRoute[0].DownstreamPathTemplate.Value);\r\n        _result.Data.Route.UpstreamTemplatePattern.Priority.ShouldBe(expected.Route.UpstreamTemplatePattern.Priority);\r\n\r\n        for (var i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++)\r\n        {\r\n            _result.Data.TemplatePlaceholderNameAndValues[i].Name.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Name);\r\n            _result.Data.TemplatePlaceholderNameAndValues[i].Value.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Value);\r\n        }\r\n\r\n        _result.IsError.ShouldBeFalse();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteHolderTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder;\n\npublic class DownstreamRouteHolderTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        DownstreamRouteHolder holder = new();\n\n        Assert.Null(holder.Route);\n        Assert.Null(holder.TemplatePlaceholderNameAndValues);\n    }\n\n    [Fact]\n    public void Ctor_List_Route()\n    {\n        // Arrange\n        Route route = new();\n        List<PlaceholderNameAndValue> placeholders = new();\n\n        // Act\n        DownstreamRouteHolder holder = new(placeholders, route);\n\n        // Assert\n        Assert.Equal(route, holder.Route);\n        Assert.Equal(placeholders, holder.TemplatePlaceholderNameAndValues);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\n\r\nnamespace Ocelot.UnitTests.DownstreamRouteFinder;\r\n\r\nusing Ocelot.DependencyInjection;\nusing Ocelot.DownstreamRouteFinder.Finder;\n\npublic class DownstreamRouteProviderFactoryTests : UnitTest\r\n{\r\n    private readonly DownstreamRouteProviderFactory _factory;\r\n    private IInternalConfiguration _config;\r\n    private IDownstreamRouteProvider _result;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n\r\n    public DownstreamRouteProviderFactoryTests()\r\n    {\r\n        var services = new ServiceCollection();\r\n        services.AddSingleton<IPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();\r\n        services.AddSingleton<IHeaderPlaceholderNameAndValueFinder, HeaderPlaceholderNameAndValueFinder>();\r\n        services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();\r\n        services.AddSingleton<IHeadersToHeaderTemplatesMatcher, HeadersToHeaderTemplatesMatcher>();\r\n        services.AddSingleton<IQoSOptionsCreator, QoSOptionsCreator>();\r\n        services.AddSingleton<IRouteKeyCreator, RouteKeyCreator>();\r\n        services.AddSingleton<IDownstreamRouteProvider, DownstreamRouteFinder>();\r\n        services.AddSingleton<IDownstreamRouteProvider, DiscoveryDownstreamRouteFinder>();\r\n        Features.AddOcelotHeaderRouting(services); // AddSingleton<IUpstreamHeaderTemplatePatternCreator, UpstreamHeaderTemplatePatternCreator>()\r\n        var provider = services.BuildServiceProvider(true);\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n        _loggerFactory.Setup(x => x.CreateLogger<DownstreamRouteProviderFactory>()).Returns(_logger.Object);\r\n        _factory = new DownstreamRouteProviderFactory(provider, _loggerFactory.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder()\r\n    {\r\n        // Arrange\r\n        var route = new Route();\r\n        GivenTheRoutes(route);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder_when_not_dynamic_re_route_and_service_discovery_on()\r\n    {\r\n        // Arrange\r\n        var route = new Route()\r\n        {\r\n            UpstreamTemplatePattern = new UpstreamPathTemplateBuilder().WithOriginalValue(\"woot\").Build(),\r\n        };\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(\"test\").WithPort(50).WithType(\"test\").Build();\r\n        GivenTheRoutes(route, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder_as_no_service_discovery_given_no_scheme()\r\n    {\r\n        // Arrange\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(string.Empty).WithHost(\"test\").WithPort(50).Build();\r\n        GivenTheRoutes(null, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder_as_no_service_discovery_given_no_host()\r\n    {\r\n        // Arrange\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(string.Empty).WithPort(50).Build();\r\n        GivenTheRoutes(null, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder_given_no_service_discovery_port()\r\n    {\r\n        // Arrange\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(\"localhost\").WithPort(0).Build();\r\n        GivenTheRoutes(null, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_finder_given_no_service_discovery_type()\r\n    {\r\n        // Arrange\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(\"localhost\").WithPort(50).WithType(string.Empty).Build();\r\n        GivenTheRoutes(null, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_creator()\r\n    {\r\n        // Arrange\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(\"test\").WithPort(50).WithType(\"test\").Build();\r\n        GivenTheRoutes(null, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DiscoveryDownstreamRouteFinder>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_downstream_route_creator_with_dynamic_re_route()\r\n    {\r\n        // Arrange\r\n        var route = new Route();\r\n        var spConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithScheme(\"http\").WithHost(\"test\").WithPort(50).WithType(\"test\").Build();\r\n        GivenTheRoutes(route, spConfig);\r\n\r\n        // Act\r\n        _result = _factory.Get(_config);\r\n\r\n        // Assert\r\n        _result.ShouldBeOfType<DiscoveryDownstreamRouteFinder>();\r\n    }\r\n\r\n    private void GivenTheRoutes(Route route, ServiceProviderConfiguration config = null)\r\n    {\r\n        Route[] routes = route == null ? Array.Empty<Route>() : [route];\r\n        _config = new InternalConfiguration(routes)\r\n        {\r\n            ServiceProviderConfiguration = config,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher;\n\n[Trait(\"PR\", \"1312\")]\n[Trait(\"Feat\", \"360\")]\npublic class HeaderPlaceholderNameAndValueFinderTests : UnitTest\n{\n    private readonly HeaderPlaceholderNameAndValueFinder _finder = new();\n\n    [Fact]\n    public void Should_return_no_placeholders()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>();\n        var upstreamHeaders = new HeaderDictionary();\n        var expected = new List<PlaceholderNameAndValue>();\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_one_placeholder_with_value_when_no_other_text()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"country\"] = new(\"^(?i)(?<countrycode>.+)$\", \"{header:countrycode}\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"country\"] = \"PL\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_one_placeholder_with_value_when_other_text_on_the_right()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"country\"] = new(\"^(?<countrycode>.+)-V1$\", \"{header:countrycode}-V1\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"country\"] = \"PL-V1\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_one_placeholder_with_value_when_other_text_on_the_left()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"country\"] = new(\"^V1-(?<countrycode>.+)$\", \"V1-{header:countrycode}\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"country\"] = \"V1-PL\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_one_placeholder_with_value_when_other_texts_surrounding()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"country\"] = new(\"^cc:(?<countrycode>.+)-V1$\", \"cc:{header:countrycode}-V1\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"country\"] = \"cc:PL-V1\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_two_placeholders_with_text_between()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"countryAndVersion\"] = new(\"^(?i)(?<countrycode>.+)-(?<version>.+)$\", \"{header:countrycode}-{header:version}\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"countryAndVersion\"] = \"PL-v1\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n            new(\"{version}\", \"v1\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    [Fact]\n    public void Should_return_placeholders_from_different_headers()\n    {\n        // Arrange\n        var upstreamHeaderTemplates = new Dictionary<string, UpstreamHeaderTemplate>\n        {\n            [\"country\"] = new(\"^(?i)(?<countrycode>.+)$\", \"{header:countrycode}\"),\n            [\"version\"] = new(\"^(?i)(?<version>.+)$\", \"{header:version}\"),\n        };\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"country\"] = \"PL\",\n            [\"version\"] = \"v1\",\n        };\n        var expected = new List<PlaceholderNameAndValue>\n        {\n            new(\"{countrycode}\", \"PL\"),\n            new(\"{version}\", \"v1\"),\n        };\n\n        // Act\n        var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList();\n\n        // Assert\n        TheResultIs(result, expected);\n    }\n\n    private static void TheResultIs(List<PlaceholderNameAndValue> actual, List<PlaceholderNameAndValue> expected)\n    {\n        actual.ShouldNotBeNull();\n        actual.Count.ShouldBe(expected.Count);\n        actual.ForEach(x => expected.Any(e => e.Name == x.Name && e.Value == x.Value).ShouldBeTrue());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.HeaderMatcher;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher;\n\n[Trait(\"PR\", \"1312\")]\n[Trait(\"Feat\", \"360\")]\npublic class HeadersToHeaderTemplatesMatcherTests : UnitTest\n{\n    private readonly HeadersToHeaderTemplatesMatcher _matcher = new();\n\n    [Fact]\n    public void Should_match_when_no_template_headers()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"anyHeaderValue\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>();\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_match_the_same_headers()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"anyHeaderValue\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_match_the_same_headers_when_differ_case_and_case_sensitive()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"ANYHEADERVALUE\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_match_the_same_headers_when_differ_case_and_case_insensitive()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"ANYHEADERVALUE\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_match_different_headers_values()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"anyHeaderValueDifferent\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_not_match_the_same_headers_names()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeaderDifferent\"] = \"anyHeaderValue\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_match_all_the_same_headers()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"anyHeaderValue\",\n            [\"notNeededHeader\"] = \"notNeededHeaderValue\",\n            [\"secondHeader\"] = \"secondHeaderValue\",\n            [\"thirdHeader\"] = \"thirdHeaderValue\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"secondHeader\"] = new(\"^(?i)secondHeaderValue$\", \"secondHeaderValue\"),\n            [\"thirdHeader\"] = new(\"^(?i)thirdHeaderValue$\", \"thirdHeaderValue\"),\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_match_the_headers_when_one_of_them_different()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"anyHeaderValue\",\n            [\"notNeededHeader\"] = \"notNeededHeaderValue\",\n            [\"secondHeader\"] = \"secondHeaderValueDIFFERENT\",\n            [\"thirdHeader\"] = \"thirdHeaderValue\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"secondHeader\"] = new(\"^(?i)secondHeaderValue$\", \"secondHeaderValue\"),\n            [\"thirdHeader\"] = new(\"^(?i)thirdHeaderValue$\", \"thirdHeaderValue\"),\n            [\"anyHeader\"] = new(\"^(?i)anyHeaderValue$\", \"anyHeaderValue\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_match_the_header_with_placeholder()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"PL\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)(?<countrycode>.+)$\", \"{header:countrycode}\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_match_the_header_with_placeholders()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"PL-V1\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)(?<countrycode>.+)-(?<version>.+)$\", \"{header:countrycode}-{header:version}\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_match_the_header_with_placeholders()\n    {\n        // Arrange\n        var upstreamHeaders = new HeaderDictionary()\n        {\n            [\"anyHeader\"] = \"PL\",\n        };\n        var templateHeaders = new Dictionary<string, UpstreamHeaderTemplate>()\n        {\n            [\"anyHeader\"] = new(\"^(?i)(?<countrycode>.+)-(?<version>.+)$\", \"{header:countrycode}-{header:version}\"),\n        };\n\n        // Act\n        var result = _matcher.Match(upstreamHeaders, templateHeaders);\n\n        // Assert\n        result.ShouldBeFalse();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValueTests.cs",
    "content": "﻿using Ocelot.DownstreamRouteFinder.UrlMatcher;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher;\n\npublic sealed class PlaceholderNameAndValueTests\n{\n    [Fact]\n    public void Key()\n    {\n        // Arrange\n        PlaceholderNameAndValue placeholder = new(\"{test}\", \"testing\");\n\n        // Act\n        var actual = placeholder.Key;\n\n        // Assert\n        Assert.Equal(\"testing\", placeholder.Value);\n        Assert.Equal(\"{test}\", placeholder.Name);\n        Assert.Equal(\"test\", actual);\n    }\n\n    [Fact]\n    public void ToString_Override()\n    {\n        // Arrange\n        PlaceholderNameAndValue placeholder = new(\"{test}\", \"testing\");\n\n        // Act\n        var actual = placeholder.ToString();\n\n        // Assert\n        Assert.Equal(\"testing\", placeholder.Value);\n        Assert.Equal(\"{test}\", placeholder.Name);\n        Assert.Equal(\"[{test}=testing]\", actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs",
    "content": "using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Values;\nusing System.Text.RegularExpressions;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher;\n\npublic class RegExUrlMatcherTests : UnitTest\n{\n    private readonly RegExUrlMatcher _matcher = new();\n    private static readonly string Empty = string.Empty;\n\n    [Fact]\n    public void Should_not_match()\n    {\n        // Arrange\n        const string path = \"/api/v1/aaaaaaaaa/cards\";\n        const string downstreamPathTemplate = \"^(?i)/api/v[^/]+/cards$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_match()\n    {\n        // Arrange\n        const string path = \"/api/v1/cards\";\n        const string downstreamPathTemplate = \"^(?i)/api/v[^/]+/cards$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_match_path_with_no_query_string()\n    {\n        // Arrange\n        const string regExForwardSlashAndOnePlaceHolder = \"^(?i)/newThing$\";\n        const string path = \"/newThing\";\n        const string queryString = \"?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-\";\n        const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder;\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, queryString, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_match_query_string()\n    {\n        // Arrange\n        const string regExForwardSlashAndOnePlaceHolder = \"^(?i)/api/subscriptions/[^/]+/updates\\\\?unitId=.+$\";\n        const string path = \"/api/subscriptions/1/updates\";\n        const string queryString = \"?unitId=2\";\n        const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder;\n        const bool containsQueryString = true;\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate, containsQueryString);\n\n        // Act\n        var result = _matcher.Match(path, queryString, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_match_query_string_with_multiple_params()\n    {\n        // Arrange\n        const string regExForwardSlashAndOnePlaceHolder = \"^(?i)/api/subscriptions/[^/]+/updates\\\\?unitId=.+&productId=.+$\";\n        const string path = \"/api/subscriptions/1/updates?unitId=2\";\n        const string queryString = \"?unitId=2&productId=2\";\n        const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder;\n        const bool containsQueryString = true;\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate, containsQueryString);\n\n        // Act\n        var result = _matcher.Match(path, queryString, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_match_slash_becaue_we_need_to_match_something_after_it()\n    {\n        // Arrange\n        const string regExForwardSlashAndOnePlaceHolder = \"^/[0-9a-zA-Z].+\";\n        const string path = \"/\";\n        const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder;\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_not_match_forward_slash_only_regex()\n    {\n        // Arrange\n        const string path = \"/working/\";\n        const string downstreamPathTemplate = \"^/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_not_match_issue_134()\n    {\n        // Arrange\n        const string path = \"/api/vacancy/1/\";\n        const string downstreamPathTemplate = \"^(?i)/vacancy/[^/]+/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_match_forward_slash_only_regex()\n    {\n        // Arrange\n        const string path = \"/\";\n        const string downstreamPathTemplate = \"^/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_find_match_when_template_smaller_than_valid_path()\n    {\n        // Arrange\n        const string path = \"/api/products/2354325435624623464235\";\n        const string downstreamPathTemplate = \"^/api/products/.+$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_find_match()\n    {\n        // Arrange\n        const string path = \"/api/values\";\n        const string downstreamPathTemplate = \"^/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url()\n    {\n        // Arrange\n        const string path = \"\";\n        const string downstreamPathTemplate = \"^$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_no_slash()\n    {\n        // Arrange\n        const string path = \"api\";\n        const string downstreamPathTemplate = \"^api$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_one_slash()\n    {\n        // Arrange\n        const string path = \"api/\";\n        const string downstreamPathTemplate = \"^api/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template()\n    {\n        // Arrange\n        const string path = \"api/product/products/\";\n        const string downstreamPathTemplate = \"^api/product/products/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_one_place_holder()\n    {\n        // Arrange\n        const string path = \"api/product/products/1\";\n        const string downstreamPathTemplate = \"^api/product/products/.+$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders()\n    {\n        // Arrange\n        const string path = \"api/product/products/1/2\";\n        const string downstreamPathTemplate = \"^api/product/products/[^/]+/.+$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something()\n    {\n        // Arrange\n        const string path = \"api/product/products/1/categories/2\";\n        const string downstreamPathTemplate = \"^api/product/products/[^/]+/categories/.+$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something()\n    {\n        // Arrange\n        const string path = \"api/product/products/1/categories/2/variant/123\";\n        const string downstreamPathTemplate = \"^api/product/products/[^/]+/categories/[^/]+/variant/.+$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders()\n    {\n        // Arrange\n        const string path = \"api/product/products/1/categories/2/variant/\";\n        const string downstreamPathTemplate = \"^api/product/products/[^/]+/categories/[^/]+/variant/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_ignore_case_sensitivity()\n    {\n        // Arrange\n        const string path = \"API/product/products/1/categories/2/variant/\";\n        const string downstreamPathTemplate = \"^(?i)api/product/products/[^/]+/categories/[^/]+/variant/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_respect_case_sensitivity()\n    {\n        // Arrange\n        const string path = \"API/product/products/1/categories/2/variant/\";\n        const string downstreamPathTemplate = \"^api/product/products/[^/]+/categories/[^/]+/variant/$\";\n        var upt = GivenUpstreamPathTemplate(downstreamPathTemplate);\n\n        // Act\n        var result = _matcher.Match(path, Empty, upt);\n\n        // Assert\n        result.Match.ShouldBeFalse();\n    }\n\n    private static UpstreamPathTemplate GivenUpstreamPathTemplate(string downstreamPathTemplate, bool containsQueryString = false)\n        => new(downstreamPathTemplate, 0, containsQueryString, downstreamPathTemplate)\n        {\n            Pattern = new Regex(downstreamPathTemplate),\n        };\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs",
    "content": "using Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher;\n\npublic class UrlPathPlaceholderNameAndValueFinderTests : UnitTest\n{\n    private readonly UrlPathPlaceholderNameAndValueFinder _finder = new();\n    private Response<List<PlaceholderNameAndValue>> _result;\n    private static readonly string Empty = string.Empty;\n\n    [Fact]\n    public void Can_match_down_stream_url()\n    {\n        // Arrange, Act\n        _result = _finder.Find(Empty, Empty, Empty);\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_nothing_then_placeholder_no_value_is_blank()\n    {\n        // Arrange, Act\n        _result = _finder.Find(Empty, Empty, \"/{url}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{url}\", Empty);\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_nothing_then_placeholder_value_is_test()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/test\", Empty, \"/{url}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{url}\", \"test\");\n    }\n\n    [Fact]\n    public void Should_match_everything_in_path_with_query()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/test/toot\", \"?$filter=Name%20eq%20'Sam'\", \"/{everything}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{everything}\", \"test/toot\");\n    }\n\n    [Fact]\n    public void Should_match_everything_in_path()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/test/toot\", Empty, \"/{everything}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{everything}\", \"test/toot\");\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_forward_slash_then_placeholder_no_value_is_blank()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/\", Empty, \"/{url}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{url}\", Empty);\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_forward_slash()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/\", Empty, \"/\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_forward_slash_then_placeholder_then_another_value()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/1/products\", Empty, \"/{url}/products\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{url}\", \"1\");\n    }\n\n    [Fact]\n    public void Should_not_find_anything()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products\", Empty, \"/products/\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Should_find_query_string()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products\", \"?productId=1\", \"/products?productId={productId}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{productId}\", \"1\");\n    }\n\n    [Fact]\n    public void Should_find_query_string_dont_include_hardcoded()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products\", \"?productId=1&categoryId=2\", \"/products?productId={productId}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{productId}\", \"1\");\n    }\n\n    [Fact]\n    public void Should_find_multiple_query_string()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products\", \"?productId=1&categoryId=2\", \"/products?productId={productId}&categoryId={categoryId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"));\n    }\n\n    [Fact]\n    public void Should_find_multiple_query_string_and_path()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products/3\", \"?productId=1&categoryId=2\", \"/products/{account}?productId={productId}&categoryId={categoryId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"),\n            new(\"{account}\", \"3\"));\n    }\n\n    [Fact]\n    public void Should_find_multiple_query_string_and_path_that_ends_with_slash()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/products/3/\", \"?productId=1&categoryId=2\", \"/products/{account}/?productId={productId}&categoryId={categoryId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"),\n            new(\"{account}\", \"3\"));\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_no_slash()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api\", Empty, \"api\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_one_slash()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/\", Empty, \"api/\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/\", Empty, \"api/product/products/\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre();\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_one_place_holder()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/1\", Empty, \"api/product/products/{productId}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{productId}\", \"1\");\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/1/2\", Empty, \"api/product/products/{productId}/{categoryId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"));\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/1/categories/2\", Empty, \"api/product/products/{productId}/categories/{categoryId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"));\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/1/categories/2/variant/123\", Empty, \"api/product/products/{productId}/categories/{categoryId}/variant/{variantId}\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"),\n            new(\"{variantId}\", \"123\"));\n    }\n\n    [Fact]\n    public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders()\n    {\n        // Arrange, Act\n        _result = _finder.Find(\"api/product/products/1/categories/2/variant/\", Empty, \"api/product/products/{productId}/categories/{categoryId}/variant/\");\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(\"{productId}\", \"1\"),\n            new(\"{categoryId}\", \"2\"));\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"89\")]\n    [InlineData(\"/api/{finalUrlPath}\", \"/api/product/products/categories/\", \"{finalUrlPath}\", \"product/products/categories/\")]\n    [InlineData(\"/myApp1Name/api/{urlPath}\", \"/myApp1Name/api/products/1\", \"{urlPath}\", \"products/1\")]\n    public void Can_match_down_stream_url_with_downstream_template_with_place_holder_to_final_url_path(string template, string path, string placeholderName, string placeholderValue)\n    {\n        // Arrange, Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenSinglePlaceholderIs(placeholderName, placeholderValue);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"748\")]\n    public void Check_for_placeholder_at_end_of_template() \n    {\n        // Arrange, Act\n        _result = _finder.Find(\"/upstream/test/\", Empty, \"/upstream/test/{testId}\");\n\n        // Assert\n        ThenSinglePlaceholderIs(\"{testId}\", Empty);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"748\")]\n    [InlineData(\"/api/invoices/{url}\", \"/api/invoices/123\", \"{url}\", \"123\")]\n    [InlineData(\"/api/invoices/{url}\", \"/api/invoices/\", \"{url}\", \"\")]\n    [InlineData(\"/api/invoices/{url}\", \"/api/invoices\", \"{url}\", \"\")]\n    [InlineData(\"/api/{version}/invoices/\", \"/api/v1/invoices/\", \"{version}\", \"v1\")]\n    public void Should_fix_issue_748(string template, string path, string placeholderName, string placeholderValue)\n    {\n        // Arrange, Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenSinglePlaceholderIs(placeholderName, placeholderValue);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"748\")]\n    [InlineData(\"/api/{version}/invoices/{url}\", \"/api/v1/invoices/123\", \"{version}\", \"v1\", \"{url}\", \"123\")]\n    [InlineData(\"/api/{version}/invoices/{url}\", \"/api/v1/invoices/\", \"{version}\", \"v1\", \"{url}\", \"\")]\n    [InlineData(\"/api/invoices/{url}?{query}\", \"/api/invoices/test?query=1\", \"{url}\", \"test\", \"{query}\", \"query=1\")]\n    [InlineData(\"/api/invoices/{url}?{query}\", \"/api/invoices/?query=1\", \"{url}\", \"\", \"{query}\", \"query=1\")]\n    public void Should_resolve_catchall_at_end_with_middle_placeholder(string template, string path, string placeholderName, string placeholderValue, string catchallName, string catchallValue)\n    {\n        // Arrange, Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenTheTemplatesVariablesAre(\n            new(placeholderName, placeholderValue),\n            new(catchallName, catchallValue));\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2199\")]\n    [InlineData(\"/api/invoices/{url}-abcd\", \"/api/invoices/123-abcd\", \"{url}\", \"123\")]\n    [InlineData(\"/api/invoices/{url1}-{url2}_abcd\", \"/api/invoices/123-456_abcd\", \"{url1}\", \"123\")]\n    public void Can_match_between_slashes(string template, string path, string placeholderName, string placeholderValue)\n    {\n        // Arrange, Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenSinglePlaceholderIs(placeholderName, placeholderValue);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2199\")]\n    [Trait(\"Feat\", \"2200\")]\n    [InlineData(\"/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}\", \"/api/invoices_super/123-456_abcd/789?urlId=987\",\n        \"{url0}\", \"super\", \"{url1}\", \"123\", \"{url2}\", \"456\", \"{url3}\", \"789\", \"{url4}\", \"987\")]\n    [InlineData(\"/api/users/{userId}/posts/{postId}_abcd/{timestamp}?filter={filter}\", \"/api/users/101/posts/2022_abcd/2024?filter=active\",\n        \"{userId}\", \"101\", \"{postId}\", \"2022\", \"{timestamp}\", \"2024\", \"{filter}\", \"active\")]\n    [InlineData(\"/api/categories/{categoryId}/{subCategoryId}_abcd/{itemId}?sort={sortBy}\", \"/api/categories/1/2_abcd/5?sort=desc\",\n        \"{categoryId}\", \"1\", \"{subCategoryId}\", \"2\", \"{itemId}\", \"5\", \"{sortBy}\", \"desc\")]\n    [InlineData(\"/api/products/{productId}/{category}_{itemId}_details/{status}\", \"/api/products/789/electronics_123_details/available\",\n        \"{productId}\", \"789\", \"{category}\", \"electronics\", \"{itemId}\", \"123\", \"{status}\", \"available\")]\n    public void Can_match_all_placeholders_between_slashes(string template, string path,\n        string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2,\n        string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4,\n        string placeholderName5 = null, string placeholderValue5 = null)\n    {\n        // Arrange\n        var expectedTemplates = new List<PlaceholderNameAndValue>\n        {\n            new(placeholderName1, placeholderValue1),\n            new(placeholderName2, placeholderValue2),\n            new(placeholderName3, placeholderValue3),\n            new(placeholderName4, placeholderValue4),\n        };\n\n        // Add optional placeholders if they exist\n        if (!string.IsNullOrEmpty(placeholderName5))\n        {\n            expectedTemplates.Add(new(placeholderName5, placeholderValue5));\n        }\n\n        // Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenTheTemplatesVariablesAre(expectedTemplates.ToArray());\n    }\n    \n    [Theory]\n    [Trait(\"Bug\", \"2209\")]\n    [InlineData(\n        \"/entities/{id}/events/recordsdata/{subCategoryId}_abcd/{itemId}?sort={sortBy}\",\n        \"/Entities/43/Events/RecordsData/2_abcd/5?sort=desc\",\n        \"{id}\", \"43\", \"{subCategoryId}\", \"2\", \"{itemId}\", \"5\", \"{sortBy}\", \"desc\")]\n    [InlineData(\n        \"/api/PRODUCTS/{productId}/{category}_{itemId}_DeTails/{status}\",\n        \"/API/Products/789/electronics_123_details/available\",\n        \"{productId}\", \"789\", \"{category}\", \"electronics\", \"{itemId}\", \"123\", \"{status}\", \"available\")]\n    public void Find_CaseInsensitive_MatchedAllPlaceholdersBetweenSlashes(string template, string path,\n        string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2,\n        string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4)\n    {\n        // Arrange\n        var expectedTemplates = new List<PlaceholderNameAndValue>\n        {\n            new(placeholderName1, placeholderValue1),\n            new(placeholderName2, placeholderValue2),\n            new(placeholderName3, placeholderValue3),\n            new(placeholderName4, placeholderValue4),\n        };\n\n        // Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenTheTemplatesVariablesAre(expectedTemplates.ToArray());\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2209\")]\n    [InlineData(\n        \"/entities/{Id}/events/recordsdata/{subCategoryId}_abcd/{itemId}?sort={sortBy}\",\n        \"/Entities/43/Events/RecordsData/2_abcd/5?sort=desc\",\n        \"{id}\", \"43\", \"{subcategoryid}\", \"2\", \"{itemid}\", \"5\", \"{sortby}\", \"desc\")]\n    [InlineData(\n        \"/api/PRODUCTS/{productid}/{category}_{itemid}_DeTails/{status}\",\n        \"/API/Products/789/electronics_123_details/available\",\n        \"{productId}\", \"789\", \"{Category}\", \"electronics\", \"{itemId}\", \"123\", \"{Status}\", \"available\")]\n    public void Find_CaseInsensitive_CannotMatchPlaceholders(string template, string path,\n        string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2,\n        string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4)\n    {\n        // Arrange\n        var expectedTemplates = new List<PlaceholderNameAndValue>\n        {\n            new(placeholderName1, placeholderValue1),\n            new(placeholderName2, placeholderValue2),\n            new(placeholderName3, placeholderValue3),\n            new(placeholderName4, placeholderValue4),\n        };\n\n        // Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert;\n        ThenTheExpectedVariablesCantBeFound(expectedTemplates.ToArray());\n    }\n    \n    [Theory]\n    [Trait(\"Bug\", \"2212\")]\n    [InlineData(\"/dati-registri/{version}/{everything}\", \"/dati-registri/v1.0/operatore/R80QQ5J9600/valida\", \"{version}\", \"v1.0\", \"{everything}\", \"operatore/R80QQ5J9600/valida\")]\n    [InlineData(\"/api/invoices/{invoiceId}/{url}\", \"/api/invoices/1\", \"{invoiceId}\", \"1\", \"{url}\", \"\")]\n    [InlineData(\"/api/{version}/{type}/{everything}\", \"/api/v1.0/items/details/12345\", \"{version}\", \"v1.0\", \"{type}\", \"items\", \"{everything}\", \"details/12345\")]\n    [InlineData(\"/resources/{area}/{id}/{details}\", \"/resources/europe/56789/info/about\", \"{area}\", \"europe\", \"{id}\", \"56789\", \"{details}\", \"info/about\")]\n    [InlineData(\"/data/{version}/{category}/{subcategory}/{rest}\", \"/data/2.1/sales/reports/weekly/summary\", \"{version}\", \"2.1\", \"{category}\", \"sales\", \"{subcategory}\", \"reports\", \"{rest}\", \"weekly/summary\")]\n    [InlineData(\"/users/{region}/{team}/{userId}/{details}\", \"/users/north/eu/12345/activities/list\", \"{region}\", \"north\", \"{team}\", \"eu\", \"{userId}\", \"12345\", \"{details}\", \"activities/list\")]\n    public void Find_HasCatchAll_OnlyTheLastPlaceholderCanContainSlashes(string template, string path,\n        string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2,\n        string placeholderName3 = null, string placeholderValue3 = null, string placeholderName4 = null, string placeholderValue4 = null)\n    {\n        // Arrange\n        var expectedTemplates = new List<PlaceholderNameAndValue>\n        {\n            new(placeholderName1, placeholderValue1),\n            new(placeholderName2, placeholderValue2),\n        };\n        \n        if (!string.IsNullOrEmpty(placeholderName3))\n        {\n            expectedTemplates.Add(new(placeholderName3, placeholderValue3));\n        }\n        \n        if (!string.IsNullOrEmpty(placeholderName4))\n        {\n            expectedTemplates.Add(new(placeholderName4, placeholderValue4));\n        }\n\n        // Act\n        _result = _finder.Find(path, Empty, template);\n\n        // Assert\n        ThenTheTemplatesVariablesAre(expectedTemplates.ToArray());\n    }\n\n    private void ThenSinglePlaceholderIs(string expectedName, string expectedValue)\n    {\n        var item = _result.Data.Single(t => t.Name == expectedName);\n        item.Value.ShouldBe(expectedValue);\n    }\n\n    private void ThenTheTemplatesVariablesAre(params PlaceholderNameAndValue[] collection)\n    {\n        foreach (var expected in collection)\n        {\n            ThenSinglePlaceholderIs(expected.Name, expected.Value);\n        }\n    }\n\n    private void ThenTheExpectedVariablesCantBeFound(params PlaceholderNameAndValue[] collection)\n    {\n        foreach (var expected in collection)\n        {\n            _result.Data.FirstOrDefault(t => t.Name == expected.Name).ShouldBeNull();\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.DownstreamUrlCreator;\n\nnamespace Ocelot.UnitTests.DownstreamUrlCreator;\n\npublic class DownstreamPathPlaceholderReplacerTests : UnitTest\n{\n    private readonly DownstreamPathPlaceholderReplacer _replacer = new();\n\n    [Fact]\n    public void Can_replace_no_template_variables()\n    {\n        // Arrange\n        var holder = new DownstreamRouteHolder(\n                new List<PlaceholderNameAndValue>(),\n                GivenRoute());\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(string.Empty);\n    }\n\n    [Fact]\n    public void Can_replace_no_template_variables_with_slash()\n    {\n        // Arrange\n        var holder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            GivenRoute(\"/\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"/\");\n    }\n\n    [Fact]\n    public void Can_replace_url_no_slash()\n    {\n        // Arrange\n        var holder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            GivenRoute(\"api\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"api\");\n    }\n\n    [Fact]\n    public void Can_replace_url_one_slash()\n    {\n        // Arrange\n        var holder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            GivenRoute(\"api/\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"api/\");\n    }\n\n    [Fact]\n    public void Can_replace_url_multiple_slash()\n    {\n        // Arrange\n        var holder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            GivenRoute(\"api/product/products/\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"api/product/products/\");\n    }\n\n    [Fact]\n    public void Can_replace_url_one_template_variable()\n    {\n        // Arrange\n        var templateVariables = new List<PlaceholderNameAndValue>\n        {\n            new(\"{productId}\", \"1\"),\n        };\n        var holder = new DownstreamRouteHolder(\n            templateVariables,\n            GivenRoute(\"productservice/products/{productId}/\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"productservice/products/1/\");\n    }\n\n    [Fact]\n    public void Can_replace_url_one_template_variable_with_path_after()\n    {\n        // Arrange\n        var templateVariables = new List<PlaceholderNameAndValue>\n        {\n            new(\"{productId}\", \"1\"),\n        };\n        var holder = new DownstreamRouteHolder(\n            templateVariables,\n            GivenRoute(\"productservice/products/{productId}/variants\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"productservice/products/1/variants\");\n    }\n\n    [Fact]\n    public void Can_replace_url_two_template_variable()\n    {\n        // Arrange\n        var templateVariables = new List<PlaceholderNameAndValue>\n        {\n            new(\"{productId}\", \"1\"),\n            new(\"{variantId}\", \"12\"),\n        };\n        var holder = new DownstreamRouteHolder(\n            templateVariables,\n            GivenRoute(\"productservice/products/{productId}/variants/{variantId}\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"productservice/products/1/variants/12\");\n    }\n\n    [Fact]\n    public void Can_replace_url_three_template_variable()\n    {\n        // Arrange\n        var templateVariables = new List<PlaceholderNameAndValue>\n        {\n            new(\"{productId}\", \"1\"),\n            new(\"{variantId}\", \"12\"),\n            new(\"{categoryId}\", \"34\"),\n        };\n        var holder = new DownstreamRouteHolder(\n            templateVariables,\n            GivenRoute(\"productservice/category/{categoryId}/products/{productId}/variants/{variantId}\"));\n\n        // Act\n        var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues);\n\n        // Assert\n        dsPath.Value.ShouldBe(\"productservice/category/34/products/1/variants/12\");\n    }\n\n    private static Route GivenRoute(string downstream = null, string method = null)\n    {\n        var route = GivenDownstreamRoute(downstream, method);\n        return new()\n        {\n            DownstreamRoute = [route],\n            UpstreamHttpMethod = [method is null ? HttpMethod.Get : new(method)],\n        };\n    }\n\n    private static DownstreamRoute GivenDownstreamRoute(string downstream = null, string method = null)\n        => new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(downstream)\n            .WithUpstreamHttpMethod([method ?? HttpMethods.Get])\n            .Build();\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.DownstreamUrlCreator;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.DownstreamUrlCreator;\n\npublic sealed class DownstreamUrlCreatorMiddlewareTests : UnitTest\n{\n    // TODO: Convert to integration tests to use real IDownstreamPathPlaceholderReplacer service (no mocking). There are a lot of failings\n    // private readonly IDownstreamPathPlaceholderReplacer _replacer;\n    private readonly Mock<IDownstreamPathPlaceholderReplacer> _replacer;\n\n    private DownstreamPath _downstreamPath;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly DownstreamUrlCreatorMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly HttpRequestMessage _request;\n    private readonly DefaultHttpContext _httpContext;\n\n    public DownstreamUrlCreatorMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<DownstreamUrlCreatorMiddleware>()).Returns(_logger.Object);\n        _replacer = new Mock<IDownstreamPathPlaceholderReplacer>();\n        _request = new HttpRequestMessage(HttpMethod.Get, \"https://my.url/abc/?q=123\");\n        _next = context => Task.CompletedTask;\n        _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _replacer.Object);\n    }\n\n    [Fact]\n    public async Task Should_replace_scheme_and_path()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"any old string\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://my.url/abc?q=123\");\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"/api/products/1\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://my.url:80/api/products/1?q=123\");\n        ThenTheQueryStringIs(\"?q=123\");\n    }\n\n    [Fact]\n    public async Task ShouldThrowNotSupportedException_WhenReplacerReturnedEmpty()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"/\" + TestName())\n            .WithUpstreamHttpMethod([\"Get\"])\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://my.url/abc?q=123\");\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"/api/products/1\");\n        _downstreamPath = new DownstreamPath(string.Empty);\n        _replacer\n            .Setup(x => x.Replace(It.IsAny<string>(), It.IsAny<List<PlaceholderNameAndValue>>()))\n            .Returns(_downstreamPath);\n\n        // Act, Assert\n        var ex = await Assert.ThrowsAsync<NotSupportedException>(() => _middleware.Invoke(_httpContext));\n        Assert.Equal(\"IDownstreamPathPlaceholderReplacerProxy returned an empty DownstreamPath for the route /ShouldThrowNotSupportedException_WhenReplacerReturnedEmpty.\", ex.Message);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public async Task Should_replace_query_string()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"/api/units/{subscriptionId}/{unitId}/updates\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{subscriptionId}\", \"1\"),\n                new(\"{unitId}\", \"2\"),\n            },\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000/api/subscriptions/1/updates?unitId=2\");\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"api/units/1/2/updates\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://localhost:5000/api/units/1/2/updates\");\n        ThenTheQueryStringIs(string.Empty);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public async Task Should_replace_query_string_but_leave_non_placeholder_queries()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"/api/units/{subscriptionId}/{unitId}/updates\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{subscriptionId}\", \"1\"),\n                new(\"{unitId}\", \"2\"),\n            },\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2\"); // unitId is the first\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"api/units/1/2/updates\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://localhost:5000/api/units/1/2/updates?productId=2\");\n        ThenTheQueryStringIs(\"?productId=2\");\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1288\")]\n    public async Task Should_replace_query_string_but_leave_non_placeholder_queries_2()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"/api/units/{subscriptionId}/{unitId}/updates\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{subscriptionId}\", \"1\"),\n                new(\"{unitId}\", \"2\"),\n            },\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2\"); // unitId is the second\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"api/units/1/2/updates\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://localhost:5000/api/units/1/2/updates?productId=2\");\n        ThenTheQueryStringIs(\"?productId=2\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"467\")]\n    public async Task Should_replace_query_string_exact_match()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{subscriptionId}\", \"1\"),\n                new(\"{unitId}\", \"2\"),\n                new(\"{unitIdIty}\", \"3\"),\n            },\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3\");\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"api/units/1/2/updates/3\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://localhost:5000/api/units/1/2/updates/3\");\n        ThenTheQueryStringIs(string.Empty);\n    }\n\n    [Fact]\n    public async Task Should_not_create_service_fabric_url()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"any old string\")\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .WithDownstreamScheme(\"https\")\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute, HttpMethod.Get)));\n        GivenTheDownstreamRequestUriIs(\"http://my.url/abc?q=123\");\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(\"/api/products/1\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://my.url:80/api/products/1?q=123\");\n    }\n\n    [Fact]\n    public async Task Should_create_service_fabric_url()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamScheme(\"http\")\n            .WithServiceName(\"Ocelot/OcelotApp\")\n            .Build();\n        var downstreamRouteHolder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute));\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(downstreamRouteHolder);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheDownstreamRequestUriIs(\"http://localhost:19081\");\n        GivenTheUrlReplacerWillReturnSequence(\"/api/products/1\", \"Ocelot/OcelotApp\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:19081/Ocelot/OcelotApp/api/products/1\");\n    }\n\n    [Fact]\n    public async Task Should_create_service_fabric_url_with_query_string_for_stateless_service()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamScheme(\"http\")\n            .WithServiceName(\"Ocelot/OcelotApp\")\n            .Build();\n        var downstreamRouteHolder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute));\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(downstreamRouteHolder);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheDownstreamRequestUriIs(\"http://localhost:19081?Tom=test&laura=1\");\n        GivenTheUrlReplacerWillReturnSequence(\"/api/products/1\", \"Ocelot/OcelotApp\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1\");\n    }\n\n    [Fact]\n    public async Task Should_create_service_fabric_url_with_query_string_for_stateful_service()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamScheme(\"http\")\n            .WithServiceName(\"Ocelot/OcelotApp\")\n            .Build();\n        var downstreamRouteHolder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute));\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(downstreamRouteHolder);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheDownstreamRequestUriIs(\"http://localhost:19081?PartitionKind=test&PartitionKey=1\");\n        GivenTheUrlReplacerWillReturnSequence(\"/api/products/1\", \"Ocelot/OcelotApp\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1\");\n    }\n\n    [Fact]\n    public async Task Should_create_service_fabric_url_with_version_from_upstream_path_template()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithDownstreamScheme(\"http\")\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(\"/products\").Build())\n            .WithServiceName(\"Service_1.0/Api\")\n            .Build();\n        var routeHolder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route));\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(routeHolder);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheDownstreamRequestUriIs(\"http://localhost:19081?PartitionKind=test&PartitionKey=1\");\n        GivenTheUrlReplacerWillReturnSequence(\"/products\", \"Service_1.0/Api\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1\");\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"473\")]\n    public async Task Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different()\n    {\n        // Arrange\n        var methods = new List<string> { \"Post\", \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/uc/Authorized/{servak}/{action}\").Build())\n            .WithDownstreamPathTemplate(\"/Authorized/{action}?server={servak}\")\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{action}\", \"1\"),\n                new(\"{servak}\", \"2\"),\n            },\n            new Route(downstreamRoute)));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn(\"/Authorized/1?server=2\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:5000/Authorized/1?server=2&refreshToken=123456789\");\n        ThenTheQueryStringIs(\"?server=2&refreshToken=123456789\");\n    }\n\n    [Fact]\n    public async Task Should_not_replace_by_empty_scheme()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamScheme(string.Empty)\n            .WithServiceName(\"Ocelot/OcelotApp\")\n            .Build();\n        var downstreamRouteHolder = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(downstreamRoute));\n        var config = new ServiceProviderConfigurationBuilder()\n            .WithType(\"ServiceFabric\")\n            .WithHost(\"localhost\")\n            .WithPort(19081)\n            .Build();\n        GivenTheDownStreamRouteIs(downstreamRouteHolder);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheDownstreamRequestUriIs(\"https://localhost:19081?PartitionKind=test&PartitionKey=1\");\n        GivenTheUrlReplacerWillReturnSequence(\"/api/products/1\", \"Ocelot/OcelotApp\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1\");\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"952\")]\n    public async Task Should_map_query_parameters_with_different_names()\n    {\n        // Arrange\n        var methods = new List<string> { \"Post\", \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/users?userId={userId}\").Build())\n            .WithDownstreamPathTemplate(\"/persons?personId={userId}\")\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{userId}\", \"webley\"),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) }\n        ));\n        GivenTheDownstreamRequestUriIs($\"http://localhost:5000/users?userId=webley\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn(\"/persons?personId=webley\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs($\"http://localhost:5000/persons?personId=webley\");\n        ThenTheQueryStringIs($\"?personId=webley\");\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"952\")]\n    public async Task Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ()\n    {\n        // Arrange\n        var methods = new List<string> { \"Post\", \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/users?userId={uid}\").Build())\n            .WithDownstreamPathTemplate(\"/persons?personId={uid}\")\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{uid}\", \"webley\"),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) }\n        ));\n        GivenTheDownstreamRequestUriIs($\"http://localhost:5000/users?userId=webley\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn(\"/persons?personId=webley\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs($\"http://localhost:5000/persons?personId=webley&userId=webley\");\n        ThenTheQueryStringIs($\"?personId=webley&userId=webley\");\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1174\")] // https://github.com/ThreeMammals/Ocelot/issues/1174\n    [InlineData(\"projectNumber=45&startDate=2019-12-12&endDate=2019-12-12\", \"projectNumber=45&startDate=2019-12-12&endDate=2019-12-12\")]\n    [InlineData(\"$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z\", \"$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00%3A00%3A00z%20and%20DateOfSale%20le%202020-03-15T00%3A00%3A00z\")]\n    public async Task Should_forward_query_parameters_without_duplicates(string everythingelse, string query)\n    {\n        // Arrange\n        var methods = new List<string> { \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/contracts?{everythingelse}\").Build())\n            .WithDownstreamPathTemplate(\"/api/contracts?{everythingelse}\")\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{everythingelse}\", everythingelse),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) }\n        ));\n        GivenTheDownstreamRequestUriIs($\"http://localhost:5000//contracts?{everythingelse}\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn($\"/api/contracts?{everythingelse}\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs($\"http://localhost:5000/api/contracts?{query}\");\n        ThenTheQueryStringIs($\"?{query}\");\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"748\")] // https://github.com/ThreeMammals/Ocelot/issues/748\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1/123\", \"{url}\", \"123\", \"/api/v1/test/123\", \"\")]\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1/123?query=1\", \"{url}\", \"123\", \"/api/v1/test/123?query=1\", \"?query=1\")]\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1/?query=1\", \"{url}\", \"\", \"/api/v1/test/?query=1\", \"?query=1\")]\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1?query=1\", \"{url}\", \"\", \"/api/v1/test?query=1\", \"?query=1\")]\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1/\", \"{url}\", \"\", \"/api/v1/test/\", \"\")]\n    [InlineData(\"/test/{version}/{url}\", \"/api/{version}/test/{url}\", \"/test/v1\", \"{url}\", \"\", \"/api/v1/test\", \"\")]\n    public async Task Should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString)\n    {\n        // Arrange\n        var methods = new List<string> { \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(upstreamTemplate).Build())\n            .WithDownstreamPathTemplate(downstreamTemplate)\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(placeholderName, placeholderValue),\n                new(\"{version}\", \"v1\"),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) }\n        ));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000\" + requestURL);\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn(downstreamURI);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:5000\" + downstreamURI);\n        ThenTheQueryStringIs(queryString);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"748\")] // https://github.com/ThreeMammals/Ocelot/issues/748\n    public async Task Should_omit_the_ending_slash_from_the_downstream_path_when_the_upstream_path_has_no_ending_slash()\n    {\n        // Arrange\n        var methods = new List<string> { \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/test/{version}/{url}\").Build())\n            .WithDownstreamPathTemplate(\"/api/{version}/test/{url}/\") // !!! ending slash\n            .WithUpstreamHttpMethod(methods)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{url}\", \"abcd\"),\n                new(\"{version}\", \"v1\"),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) }\n        ));\n        GivenTheDownstreamRequestUriIs(\"http://localhost:5000\" + \"/test/v1/abcd\"); // upstream has no ending slash\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn(\"/api/v1/test/abcd/\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(\"http://localhost:5000\" + \"/api/v1/test/abcd\");\n        ThenTheQueryStringIs(\"\");\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2002\")]\n    public async Task Should_map_when_query_parameters_has_same_names_with_placeholder()\n    {\n        // Arrange\n        const string username = \"bbenameur\";\n        const string groupName = \"Paris\";\n        const string roleid = \"123456\";\n        const string everything = \"something=9874565\";\n        var withGetMethod = new List<string> { \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}\")\n                .Build())\n            .WithDownstreamPathTemplate(\"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}\")\n            .WithUpstreamHttpMethod(withGetMethod)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{username}\", username),\n                new(\"{groupName}\", groupName),\n                new(\"{roleid}\", roleid),\n                new(\"{everything}\", everything),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) }\n        ));\n        GivenTheDownstreamRequestUriIs($\"http://localhost:5000/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn($\"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs($\"http://localhost:5000/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}\");\n        ThenTheQueryStringIs($\"?roleId={roleid}&{everything}\");\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2116\")]\n    [InlineData(\"api/debug()\")] // no query\n    [InlineData(\"api/debug%28%29\")] // debug()\n    public async Task ShouldNotFailToHandleUrlWithSpecialRegexChars(string urlPath)\n    {\n        // Arrange\n        var withGetMethod = new List<string> { \"Get\" };\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()\n                .WithOriginalValue(\"/routed/api/{path}\")\n                .Build())\n            .WithDownstreamPathTemplate(\"/api/{path}\")\n            .WithUpstreamHttpMethod(withGetMethod)\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>\n            {\n                new(\"{path}\", urlPath),\n            },\n            new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) }\n        ));\n        GivenTheDownstreamRequestUriIs($\"http://localhost:5000/{urlPath}\");\n        GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());\n        GivenTheUrlReplacerWillReturn($\"routed/{urlPath}\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs($\"http://localhost:5000/routed/{urlPath}\");\n        Assert.Equal((int)HttpStatusCode.OK, _httpContext.Response.StatusCode);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2351\")] // https://github.com/ThreeMammals/Ocelot/pull/2351\n    [Trait(\"Bug\", \"2346\")] // https://github.com/ThreeMammals/Ocelot/issues/2346\n    [InlineData(\"/v1/payment-methods\", \"{id}\",\n        \"http://localhost:5003/v1/payment-methods?customer_id=12345\", null, \"?customer_id=12345\")]\n    [InlineData(\"/v1/orders\", \"{id}\",\n        \"http://localhost:5003/v1/orders?orderid=999&customer_id=12345\", null, \"?orderid=999&customer_id=12345\")]\n    [InlineData(\"/v1/users\", \"{customer_id}\",\n        \"http://localhost:5003/v1/users?id=999\", null, \"?id=999\")]\n    [InlineData(\"/v1/records\", \"{id1}\",\n        \"http://localhost:5003/v1/records?id1=123&id10=456\", \"http://localhost:5003/v1/records?id10=456\", \"?id10=456\")]\n    public async Task Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder(\n        string downstreamPath, string placeholder, string url, string downstreamUrl, string query)\n    {\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(downstreamPath)\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .WithDownstreamScheme(Uri.UriSchemeHttp)\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamRouteIs(new DownstreamRouteHolder(\n            [ new(placeholder, \"123\") ],\n            new(downstreamRoute, HttpMethod.Get)\n        ));\n        GivenTheDownstreamRequestUriIs(url);\n        GivenTheServiceProviderConfigIs(config);\n        GivenTheUrlReplacerWillReturn(downstreamPath);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheDownstreamRequestUriIs(downstreamUrl ?? url);\n        ThenTheQueryStringIs(query);\n    }\n\n    private static ReadOnlySpan<char> GetPath(string downstreamPath)\n        => DownstreamUrlCreatorMiddlewareTestWrapper.GetPath(downstreamPath);\n\n    [Theory]\n    [InlineData(\"/api/resource?param=value\", \"/api/resource\")]\n    [InlineData(\"/api/resource?x=1&y=2\", \"/api/resource\")]\n    public void GetPath_ShouldReturnSubstringBeforeQuestionMark(string input, string expected)\n    {\n        // Act\n        var result = GetPath(input);\n\n        // Assert\n        Assert.Equal(expected, result.ToString());\n    }\n\n    [Theory]\n    [InlineData(\"/api/resource\", \"/api/resource\")]\n    [InlineData(\"/api/resource/123\", \"/api/resource/123\")]\n    public void GetPath_ShouldReturnFullPath_WhenNoQuestionMark(string input, string expected)\n    {\n        // Act\n        var result = GetPath(input);\n\n        // Assert\n        Assert.Equal(expected, result.ToString());\n    }\n\n    [Fact]\n    public void GetPath_ShouldHandleEmptyString()\n    {\n        // Act\n        var result = GetPath(string.Empty);\n\n        // Assert\n        Assert.Equal(string.Empty, result.ToString());\n    }\n\n    private static ReadOnlySpan<char> GetQueryString(string downstreamPath)\n        => DownstreamUrlCreatorMiddlewareTestWrapper.GetQueryString(downstreamPath);\n\n    [Theory]\n    [InlineData(\"/api/resource?param=value\", \"?param=value\")]\n    [InlineData(\"/api/resource?x=1&y=2\", \"?x=1&y=2\")]\n    public void GetQueryString_ShouldReturnSubstringStartingWithQuestionMark(string input, string expected)\n    {\n        // Act\n        var result = GetQueryString(input);\n\n        // Assert\n        Assert.Equal(expected, result.ToString());\n    }\n\n    [Theory]\n    [InlineData(\"/api/resource\", \"\")]\n    [InlineData(\"/api/resource/123\", \"\")]\n    public void GetQueryString_ShouldReturnEmpty_WhenNoQuestionMark(string input, string expected)\n    {\n        // Act\n        var result = GetQueryString(input);\n\n        // Assert\n        Assert.Equal(expected, result.ToString());\n    }\n\n    [Fact]\n    public void GetQueryString_ShouldHandleEmptyString()\n    {\n        // Act\n        var result = GetQueryString(string.Empty);\n\n        // Assert\n        Assert.Equal(string.Empty, result.ToString());\n    }\n\n    [Fact]\n    public void GetQueryString_ShouldHandleOnlyQuestionMark()\n    {\n        // Act\n        var result = GetQueryString(\"?\");\n\n        // Assert\n        Assert.Equal(\"?\", result.ToString());\n    }\n\n    private static HashSet<HttpMethod> AsHashSet(IEnumerable<string> collection) => collection.Select(AsHttpMethod).ToHashSet();\n    private static HttpMethod AsHttpMethod(string method) => new(method);\n\n    private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config)\n    {\n        var configuration = new InternalConfiguration()\n        {\n            ServiceProviderConfiguration = config,\n        };\n        _httpContext.Items.SetIInternalConfiguration(configuration);\n    }\n\n    private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute)\n    {\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n    }\n\n    private void GivenTheDownstreamRequestUriIs(string uri)\n    {\n        _request.RequestUri = new Uri(uri);\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request));\n    }\n\n    private void GivenTheUrlReplacerWillReturnSequence(params string[] paths)\n    {\n        var setup = _replacer\n            .SetupSequence(x => x.Replace(It.IsAny<string>(), It.IsAny<List<PlaceholderNameAndValue>>()));\n        foreach (var path in paths)\n        {\n            setup.Returns(new DownstreamPath(path));\n        }\n    }\n\n    private void GivenTheUrlReplacerWillReturn(string path)\n    {\n        _downstreamPath = new DownstreamPath(path);\n        _replacer\n            .Setup(x => x.Replace(It.IsAny<string>(), It.IsAny<List<PlaceholderNameAndValue>>()))\n            .Returns(_downstreamPath);\n    }\n\n    private void ThenTheDownstreamRequestUriIs(string expectedUri)\n    {\n        _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri);\n    }\n\n    private void ThenTheQueryStringIs(string queryString)\n    {\n        _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString);\n    }\n}\n\ninternal class DownstreamUrlCreatorMiddlewareTestWrapper : DownstreamUrlCreatorMiddleware\n{\n    public DownstreamUrlCreatorMiddlewareTestWrapper(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamPathPlaceholderReplacer replacer)\n        : base(next, loggerFactory, replacer) { }\n\n    public static new ReadOnlySpan<char> GetPath(ReadOnlySpan<char> downstreamPath)\n        => DownstreamUrlCreatorMiddleware.GetPath(downstreamPath);\n\n    public static new ReadOnlySpan<char> GetQueryString(ReadOnlySpan<char> downstreamPath)\n        => DownstreamUrlCreatorMiddleware.GetQueryString(downstreamPath);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Errors/ErrorTests.cs",
    "content": "using Ocelot.Infrastructure.RequestData;\n\r\nnamespace Ocelot.UnitTests.Errors;\r\n\r\npublic class ErrorTests\r\n{\r\n    [Fact]\r\n    public void Should_return_message()\r\n    {\r\n        // Arrange\r\n        var error = new CannotAddDataError(\"message\");\r\n\r\n        // Act\r\n        var result = error.ToString();\r\n\r\n        // Assert\r\n        result.ShouldBe(\"CannotAddDataError: message\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Errors;\nusing Ocelot.Errors.Middleware;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\n\nnamespace Ocelot.UnitTests.Errors;\n\npublic class ExceptionHandlerMiddlewareTests : UnitTest\n{\n    private bool _shouldThrowAnException;\n    private readonly Mock<IRequestScopedDataRepository> _repo;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ExceptionHandlerMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ExceptionHandlerMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _repo = new Mock<IRequestScopedDataRepository>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ExceptionHandlerMiddleware>()).Returns(_logger.Object);\n        _next = async context =>\n        {\n            await Task.CompletedTask;\n            if (_shouldThrowAnException)\n            {\n                throw new Exception(\"BOOM\");\n            }\n\n            _httpContext.Response.StatusCode = (int)HttpStatusCode.OK;\n        };\n        _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _repo.Object);\n    }\n\n    [Fact]\n    public async Task NoDownstreamException()\n    {\n        // Arrange\n        _shouldThrowAnException = false;\n        var config = new InternalConfiguration();\n        _httpContext.Items.Add(nameof(IInternalConfiguration), config);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK);\n        _repo.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n    }\n\n    [Fact]\n    public async Task DownstreamException()\n    {\n        // Arrange\n        _shouldThrowAnException = true;\n        var config = new InternalConfiguration();\n        _httpContext.Items.Add(nameof(IInternalConfiguration), config);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError);\n    }\n\n    [Fact]\n    public async Task ShouldSetRequestId()\n    {\n        // Arrange\n        _shouldThrowAnException = false;\n        var config = new InternalConfiguration()\n        {\n            RequestId = \"requestidkey\",\n        };\n        _httpContext.Items.Add(nameof(IInternalConfiguration), config);\n        _httpContext.Request.Headers.Append(\"requestidkey\", \"1234\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK);\n        _repo.Verify(x => x.Add(\"RequestId\", \"1234\"), Times.Once);\n    }\n\n    [Fact]\n    public async Task ShouldSetAspDotNetRequestId()\n    {\n        // Arrange\n        _shouldThrowAnException = false;\n        var config = new InternalConfiguration();\n        _httpContext.Items.Add(nameof(IInternalConfiguration), config);\n        _httpContext.Request.Headers.Append(\"requestidkey\", \"1234\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK);\n        _repo.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n    }\n\n    [Fact]\n    public async Task Should_throw_exception_if_config_provider_throws()\n    {\n        // Arrange\n        _shouldThrowAnException = false;\n\n        // this will break when we handle not having the configuratio in the items dictionary\n        _httpContext.Items = new Dictionary<object, object>();\n        _httpContext.Request.Headers.Append(\"requestidkey\", \"1234\");\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError);\n    }\n\n    private class FakeError : Error\n    {\n        internal FakeError()\n            : base(\"meh\", OcelotErrorCode.CannotAddDataError, 404)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Repository;\nusing Ocelot.Provider.Eureka;\nusing Ocelot.Responses;\nusing Steeltoe.Discovery;\n\nnamespace Ocelot.UnitTests.Eureka;\n\npublic class EurekaMiddlewareConfigurationProviderTests\n{\n    [Fact]\n    public void ShouldNotBuild()\n    {\n        // Arrange\n        var configRepo = new Mock<IInternalConfigurationRepository>();\n        configRepo.Setup(x => x.Get())\n            .Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration()));\n        var services = new ServiceCollection();\n        services.AddSingleton(configRepo.Object);\n        var sp = services.BuildServiceProvider(true);\n\n        // Act\n        var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp));\n\n        // Assert\n        provider.Status.ShouldBe(TaskStatus.RanToCompletion);\n    }\n\n    [Fact]\n    public void ShouldBuild()\n    {\n        // Arrange\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder().WithType(\"eureka\").Build();\n        var client = new Mock<IDiscoveryClient>();\n        var configRepo = new Mock<IInternalConfigurationRepository>();\n        configRepo.Setup(x => x.Get())\n            .Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration() { ServiceProviderConfiguration = serviceProviderConfig }));\n        var services = new ServiceCollection();\n        services.AddSingleton(configRepo.Object);\n        services.AddSingleton(client.Object);\n        var sp = services.BuildServiceProvider(true);\n\n        // Act\n        var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp));\n\n        // Assert\n        provider.Status.ShouldBe(TaskStatus.RanToCompletion);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Provider.Eureka;\nusing Steeltoe.Discovery;\n\r\nnamespace Ocelot.UnitTests.Eureka;\r\n\r\npublic class EurekaProviderFactoryTests\r\n{\r\n    [Fact]\r\n    public void Should_not_get()\r\n    {\r\n        // Arrange\r\n        var config = new ServiceProviderConfigurationBuilder().Build();\r\n        var sp = new ServiceCollection().BuildServiceProvider(true);\r\n\r\n        // Act, Assert\r\n        Should.Throw<NullReferenceException>(() => EurekaProviderFactory.Get(sp, config, null));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_get()\r\n    {\r\n        // Arrange\r\n        var config = new ServiceProviderConfigurationBuilder().WithType(\"eureka\").Build();\r\n        var client = new Mock<IDiscoveryClient>();\r\n        var services = new ServiceCollection();\r\n        services.AddSingleton(client.Object);\r\n        var sp = services.BuildServiceProvider(true);\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(string.Empty)\r\n            .Build();\r\n\r\n        // Act\r\n        var provider = EurekaProviderFactory.Get(sp, config, route);\r\n\r\n        // Assert\r\n        provider.ShouldBeOfType<Provider.Eureka.Eureka>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs",
    "content": "﻿using Ocelot.Values;\nusing Steeltoe.Common.Discovery;\nusing Steeltoe.Discovery;\nusing _Eureka_ = Ocelot.Provider.Eureka.Eureka;\n\r\nnamespace Ocelot.UnitTests.Eureka;\r\n\r\npublic class EurekaServiceDiscoveryProviderTests : UnitTest\r\n{\r\n    private readonly _Eureka_ _provider;\r\n    private readonly Mock<IDiscoveryClient> _client;\r\n    private readonly string _serviceId;\r\n    private List<Service> _result;\r\n\r\n    public EurekaServiceDiscoveryProviderTests()\r\n    {\r\n        _serviceId = \"Laura\";\r\n        _client = new Mock<IDiscoveryClient>();\r\n        _provider = new _Eureka_(_serviceId, _client.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_empty_services()\r\n    {\r\n        // Arrange, Act\r\n        _result = await _provider.GetAsync();\r\n\r\n        // Assert\r\n        _result.Count.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_service_from_client()\r\n    {\r\n        // Arrange\r\n        var instances = new List<IServiceInstance>\r\n        {\r\n            new EurekaService(_serviceId, \"somehost\", 801, false, new Uri(\"http://somehost:801\"), new Dictionary<string, string>()),\r\n        };\r\n        _client.Setup(x => x.GetInstances(It.IsAny<string>())).Returns(instances);\r\n\r\n        // Act\r\n        _result = await _provider.GetAsync();\r\n        _result.Count.ShouldBe(1);\r\n\r\n        // Assert\r\n        _client.Verify(x => x.GetInstances(_serviceId), Times.Once);\r\n\r\n        // Assert: Then The Service Is Mapped\r\n        _result[0].HostAndPort.DownstreamHost.ShouldBe(\"somehost\");\r\n        _result[0].HostAndPort.DownstreamPort.ShouldBe(801);\r\n        _result[0].Name.ShouldBe(_serviceId);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_services_from_client()\r\n    {\r\n        // Arrange\r\n        var instances = new List<IServiceInstance>\r\n        {\r\n            new EurekaService(_serviceId, \"somehost\", 801, false, new Uri(\"http://somehost:801\"), new Dictionary<string, string>()),\r\n            new EurekaService(_serviceId, \"somehost\", 801, false, new Uri(\"http://somehost:801\"), new Dictionary<string, string>()),\r\n        };\r\n        _client.Setup(x => x.GetInstances(It.IsAny<string>())).Returns(instances);\r\n\r\n        // Act\r\n        _result = await _provider.GetAsync();\r\n\r\n        // Assert\r\n        _result.Count.ShouldBe(2);\r\n        _client.Verify(x => x.GetInstances(_serviceId), Times.Once);\r\n    }\r\n}\r\n\r\npublic class EurekaService : IServiceInstance\r\n{\r\n    public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary<string, string> metadata)\r\n    {\r\n        ServiceId = serviceId;\r\n        Host = host;\r\n        Port = port;\r\n        IsSecure = isSecure;\r\n        Uri = uri;\r\n        Metadata = metadata;\r\n    }\r\n\r\n    public string ServiceId { get; }\r\n    public string Host { get; }\r\n    public int Port { get; }\r\n    public bool IsSecure { get; }\r\n    public Uri Uri { get; }\r\n    public IDictionary<string, string> Metadata { get; }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Eureka/OcelotBuilderExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Ocelot.Provider.Eureka;\nusing Ocelot.ServiceDiscovery;\nusing Steeltoe.Common.Discovery;\nusing Steeltoe.Common.Http.Discovery;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Eureka;\n\npublic sealed class OcelotBuilderExtensionsTests : UnitTest\n{\n    private readonly IServiceCollection _services;\n    private readonly IConfiguration _configRoot;\n    private IOcelotBuilder _ocelotBuilder;\n\n    public OcelotBuilderExtensionsTests()\n    {\n        _configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());\n        _services = new ServiceCollection();\n        _services.AddSingleton(GetHostingEnvironment());\n        _services.AddSingleton(_configRoot);\n    }\n\n    private static IWebHostEnvironment GetHostingEnvironment()\n    {\n        var environment = new Mock<IWebHostEnvironment>();\n        environment.Setup(e => e.ApplicationName)\n            .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name);\n        return environment.Object;\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"734\")]\n    [Trait(\"Feat\", \"324\")]\n    [Trait(\"Feat\", \"844\")]\n    public void AddEureka_NoExceptions_ShouldSetUpEureka()\n    {\n        // Arrange\n        var addOcelot = () => _ocelotBuilder = _services.AddOcelot(_configRoot);\n        addOcelot.ShouldNotThrow();\n\n        // Act\n        var addEureka = () => _ocelotBuilder.AddEureka();\n\n        // Assert\n        addEureka.ShouldNotThrow();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"734\")]\n    [Trait(\"Feat\", \"324\")]\n    [Trait(\"Feat\", \"844\")]\n    public void AddEureka_DefaultServices_HappyPath()\n    {\n        // Arrange, Act\n        _ocelotBuilder = _services.AddOcelot(_configRoot).AddEureka();\n\n        // Assert: AddDiscoveryClient\n        var descriptor = _services.SingleOrDefault(Of<DiscoveryHttpMessageHandler>).ShouldNotBeNull();\n        descriptor.Lifetime.ShouldBe(ServiceLifetime.Transient);\n        descriptor = _services.SingleOrDefault(Of<IServiceInstanceProvider>).ShouldNotBeNull();\n        descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton);\n\n        // Assert\n        descriptor = _services.SingleOrDefault(Of<ServiceDiscoveryFinderDelegate>).ShouldNotBeNull();\n        descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton);\n        descriptor = _services.SingleOrDefault(Of<OcelotMiddlewareConfigurationDelegate>).ShouldNotBeNull();\n        descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton);\n    }\n\n    private static bool Of<TType>(ServiceDescriptor descriptor)\n        where TType : class\n        => descriptor.ServiceType.Equals(typeof(TType));\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/FileUnitTest.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests;\n\npublic class FileUnitTest : FileUnit\n{\n    //protected static FileRouteBox<FileRoute> Box(FileRoute route) => new(route);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Errors;\nusing Ocelot.Headers;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Logging;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.Headers;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-headers\">Claims to Headers</see>.\n/// </summary>\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic class AddHeadersToRequestClaimToThingTests : UnitTest\n{\n    private readonly AddHeadersToRequest _addHeadersToRequest;\n    private readonly Mock<IClaimsParser> _parser;\n    private readonly DownstreamRequest _downstreamRequest;\n    private readonly Mock<IPlaceholders> _placeholders;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n\n    public AddHeadersToRequestClaimToThingTests()\n    {\n        _parser = new Mock<IClaimsParser>();\n        _placeholders = new Mock<IPlaceholders>();\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _addHeadersToRequest = new AddHeadersToRequest(_parser.Object, _placeholders.Object, _factory.Object);\n        _downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\"));\n    }\n\n    [Fact]\n    public void Should_add_headers_to_downstreamRequest()\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        var configuration = new List<ClaimToThing>\n        {\n            new(\"header-key\", string.Empty, string.Empty, 0),\n        };\n        var claimValue = GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        ThenTheHeaderIsAdded(claimValue);\n    }\n\n    [Fact]\n    public void Should_replace_existing_headers_on_request()\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        var configuration = new List<ClaimToThing>\n        {\n            new(\"header-key\", string.Empty, string.Empty, 0),\n        };\n        var claimValue = GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n        _downstreamRequest.Headers.Add(\"header-key\", \"initial\");\n\n        // Act\n        var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        ThenTheHeaderIsAdded(claimValue);\n    }\n\n    [Fact]\n    public void Should_return_error()\n    {\n        // Arrange\n        var claims = new List<Claim>();\n        var configuration = new List<ClaimToThing>\n        {\n            new(string.Empty, string.Empty, string.Empty, 0),\n        };\n        _ = GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error> { new AnyError() }));\n\n        // Act\n        var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    private Response<string> GivenTheClaimParserReturns(Response<string> claimValue)\n    {\n        _parser.Setup(x => x.GetValue(It.IsAny<IEnumerable<Claim>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))\n                .Returns(claimValue);\n        return claimValue;\n    }\n\n    private void ThenTheHeaderIsAdded(Response<string> claimValue)\n        => _downstreamRequest.Headers.First(x => x.Key == \"header-key\").Value.First().ShouldBe(claimValue.Data);\n\n    private class AnyError : Error\n    {\n        public AnyError()\n            : base(\"blahh\", OcelotErrorCode.UnknownError, 404)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Headers;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\r\nnamespace Ocelot.UnitTests.Headers;\r\n\r\npublic class AddHeadersToRequestPlainTests : UnitTest\r\n{\r\n    private readonly AddHeadersToRequest _addHeadersToRequest;\r\n    private readonly DefaultHttpContext _context;\r\n    private AddHeader _addedHeader;\r\n    private readonly Mock<IPlaceholders> _placeholders;\r\n    private readonly Mock<IOcelotLoggerFactory> _factory;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n\r\n    public AddHeadersToRequestPlainTests()\r\n    {\r\n        _placeholders = new Mock<IPlaceholders>();\r\n        _factory = new Mock<IOcelotLoggerFactory>();\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _factory.Setup(x => x.CreateLogger<AddHeadersToRequest>()).Returns(_logger.Object);\r\n        _addHeadersToRequest = new AddHeadersToRequest(Mock.Of<IClaimsParser>(), _placeholders.Object, _factory.Object);\r\n        _context = new DefaultHttpContext();\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"623\")] // https://github.com/ThreeMammals/Ocelot/issues/623\r\n    [Trait(\"PR\", \"632\")] // https://github.com/ThreeMammals/Ocelot/pull/632\r\n    public void Should_log_error_if_cannot_find_placeholder()\r\n    {\r\n        // Arrange\r\n        _placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new ErrorResponse<string>(new AnyError()));\r\n\r\n        // Act\r\n        WhenAddingHeader(\"X-Forwarded-For\", \"{RemoteIdAddress}\");\r\n\r\n        // Assert\r\n        _logger.Verify(x => x.LogWarning(It.Is<Func<string>>(y => y.Invoke() == $\"Unable to add header to response X-Forwarded-For: {{RemoteIdAddress}}\")), Times.Once);\r\n    }\r\n\r\n    [Fact]\r\n    [Trait(\"Feat\", \"623\")]\r\n    [Trait(\"PR\", \"632\")]\r\n    public void Should_add_placeholder_to_downstream_request()\r\n    {\r\n        // Arrange\r\n        _placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new OkResponse<string>(\"replaced\"));\r\n\r\n        // Act\r\n        WhenAddingHeader(\"X-Forwarded-For\", \"{RemoteIdAddress}\");\r\n\r\n        // Assert\r\n        ThenTheHeaderGetsTakenOverToTheRequestHeaders(\"replaced\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_add_plain_text_header_to_downstream_request()\r\n    {\r\n        // Arrange, Act\r\n        WhenAddingHeader(\"X-Custom-Header\", \"PlainValue\");\r\n\r\n        // Assert\r\n        ThenTheHeaderGetsTakenOverToTheRequestHeaders();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_overwrite_existing_header_with_added_header()\r\n    {\r\n        // Arrange\r\n        _context.Request.Headers.Append(\"X-Custom-Header\", \"This should get overwritten\");\r\n\r\n        // Act\r\n        WhenAddingHeader(\"X-Custom-Header\", \"PlainValue\");\r\n\r\n        // Assert\r\n        ThenTheHeaderGetsTakenOverToTheRequestHeaders();\r\n    }\r\n\r\n    private void WhenAddingHeader(string headerKey, string headerValue)\r\n    {\r\n        _addedHeader = new AddHeader(headerKey, headerValue);\r\n        _addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context);\r\n    }\r\n\r\n    private void ThenTheHeaderGetsTakenOverToTheRequestHeaders()\r\n    {\r\n        var requestHeaders = _context.Request.Headers;\r\n        requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($\"Header {_addedHeader.Key} was expected but not there.\");\r\n        var value = requestHeaders[_addedHeader.Key];\r\n        value.ShouldNotBe(default, $\"Value of header {_addedHeader.Key} was expected to not be null.\");\r\n        value.ToString().ShouldBe(_addedHeader.Value);\r\n    }\r\n\r\n    private void ThenTheHeaderGetsTakenOverToTheRequestHeaders(string expected)\r\n    {\r\n        var requestHeaders = _context.Request.Headers;\r\n        var value = requestHeaders[_addedHeader.Key];\r\n        value.ToString().ShouldBe(expected);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs",
    "content": "using Ocelot.Configuration.Creator;\nusing Ocelot.Headers;\nusing Ocelot.Infrastructure;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\nnamespace Ocelot.UnitTests.Headers;\n\npublic class AddHeadersToResponseTests : UnitTest\n{\n    private readonly AddHeadersToResponse _adder;\n    private readonly Mock<IPlaceholders> _placeholders;\n    private readonly DownstreamResponse _response;\n    private List<AddHeader> _addHeaders;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n\n    public AddHeadersToResponseTests()\n    {\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _factory.Setup(x => x.CreateLogger<AddHeadersToResponse>()).Returns(_logger.Object);\n        _placeholders = new Mock<IPlaceholders>();\n        _adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object);\n        _response = new DownstreamResponse(new HttpResponseMessage());\n    }\n\n    [Fact]\n    public void Should_add_header()\n    {\n        // Arrange\n        _addHeaders = new List<AddHeader>\n        {\n            new(\"Laura\", \"Tom\"),\n        };\n\n        // Act\n        _adder.Add(_addHeaders, _response);\n\n        // Assert\n        ThenTheHeaderIsReturned(\"Laura\", \"Tom\");\n    }\n\n    [Fact]\n    public void Should_add_trace_id_placeholder()\n    {\n        // Arrange\n        _addHeaders = new List<AddHeader>\n        {\n            new(\"Trace-Id\", \"{TraceId}\"),\n        };\n        var traceId = \"123\";\n        GivenTheTraceIdIs(traceId);\n\n        // Act\n        _adder.Add(_addHeaders, _response);\n\n        // Assert\n        ThenTheHeaderIsReturned(\"Trace-Id\", traceId);\n    }\n\n    [Fact]\n    public void Should_add_trace_id_placeholder_and_normal()\n    {\n        // Arrange\n        _addHeaders = new List<AddHeader>\n        {\n            new(\"Trace-Id\", \"{TraceId}\"),\n            new(\"Tom\", \"Laura\"),\n        };\n        var traceId = \"123\";\n        GivenTheTraceIdIs(traceId);\n\n        // Act\n        _adder.Add(_addHeaders, _response);\n\n        // Assert\n        ThenTheHeaderIsReturned(\"Trace-Id\", traceId);\n        ThenTheHeaderIsReturned(\"Tom\", \"Laura\");     \n    }\n\n    [Fact]\n    public void Should_do_nothing_and_log_error()\n    {\n        // Arrange\n        _addHeaders = new List<AddHeader>\n        {\n            new(\"Trace-Id\", \"{TraceId}\"),\n        };\n        _placeholders.Setup(x => x.Get(\"{TraceId}\")).Returns(new ErrorResponse<string>(new AnyError()));\n\n        // Act\n        _adder.Add(_addHeaders, _response);\n\n        // Assert\n        _response.Headers.Any(x => x.Key == \"Trace-Id\").ShouldBeFalse();\n        _logger.Verify(x => x.LogWarning(It.Is<Func<string>>(y => y.Invoke() == \"Unable to add header to response Trace-Id: {TraceId}\")), Times.Once);\n    }\n\n    private void GivenTheTraceIdIs(string traceId)\n    {\n        _placeholders.Setup(x => x.Get(\"{TraceId}\")).Returns(new OkResponse<string>(traceId));\n    }\n\n    private void ThenTheHeaderIsReturned(string key, string value)\n    {\n        var values = _response.Headers.First(x => x.Key == key);\n        values.Values.First().ShouldBe(value);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.Headers;\nusing Ocelot.Headers.Middleware;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.Headers;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-headers\">Claims to Headers</see>.\n/// </summary>\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic class ClaimsToHeadersMiddlewareTests : UnitTest\n{\n    private readonly Mock<IAddHeadersToRequest> _addHeaders;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ClaimsToHeadersMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ClaimsToHeadersMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _addHeaders = new Mock<IAddHeadersToRequest>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ClaimsToHeadersMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _middleware = new ClaimsToHeadersMiddleware(_next, _loggerFactory.Object, _addHeaders.Object);\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\n    }\n\n    [Fact]\n    public async Task Should_call_add_headers_to_request_correctly()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithClaimsToHeaders(new List<ClaimToThing>\n                    {\n                        new(\"UserId\", \"Subject\", string.Empty, 0),\n                    })\n                    .WithUpstreamHttpMethod([\"Get\"])\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new(),\n            new Route(route, HttpMethod.Get));\n\n        // Arrange: Given The Down Stream Route Is\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n\n        // Arrange: Given The AddHeaders To Downstream Request Returns Ok\n        _addHeaders.Setup(x => x.SetHeadersOnDownstreamRequest(It.IsAny<List<ClaimToThing>>(), It.IsAny<IEnumerable<Claim>>(), It.IsAny<DownstreamRequest>()))\n            .Returns(new OkResponse());\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert: Then The AddHeaders ToRequest Is Called Correctly\n        _addHeaders.Verify(\n            x => x.SetHeadersOnDownstreamRequest(It.IsAny<List<ClaimToThing>>(), It.IsAny<IEnumerable<Claim>>(), _httpContext.Items.DownstreamRequest()),\n            Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\r\nusing Ocelot.Headers;\r\nusing Ocelot.Responses;\n\r\nnamespace Ocelot.UnitTests.Headers;\r\n\r\npublic class HttpContextRequestHeaderReplacerTests : UnitTest\r\n{\r\n    private readonly DefaultHttpContext _context;\r\n    private readonly HttpContextRequestHeaderReplacer _replacer;\r\n\r\n    public HttpContextRequestHeaderReplacerTests()\r\n    {\r\n        _replacer = new();\r\n        _context = new();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_replace_headers()\r\n    {\r\n        // Arrange\r\n        _context.Request.Headers.Append(\"test\", \"test\");\r\n        var fAndRs = new List<HeaderFindAndReplace> { new(\"test\", \"test\", \"chiken\", 0) };\r\n\r\n        // Act\r\n        var result = _replacer.Replace(_context, fAndRs);\r\n\r\n        // Assert\r\n        result.ShouldBeOfType<OkResponse>();\r\n        foreach (var f in fAndRs)\r\n        {\r\n            _context.Request.Headers.TryGetValue(f.Key, out var values);\r\n            values[f.Index].ShouldBe(f.Replace);\r\n        }\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_replace_headers()\r\n    {\r\n        // Arrange\r\n        _context.Request.Headers.Append(\"test\", \"test\");\r\n        var fAndRs = new List<HeaderFindAndReplace>();\r\n\r\n        // Act\r\n        var result = _replacer.Replace(_context, fAndRs);\r\n\r\n        // Assert\r\n        result.ShouldBeOfType<OkResponse>();\r\n        foreach (var f in fAndRs)\r\n        {\r\n            _context.Request.Headers.TryGetValue(f.Key, out var values);\r\n            values[f.Index].ShouldBe(\"test\");\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Authorization;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Headers;\nusing Ocelot.Headers.Middleware;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.UnitTests.Headers;\n\npublic class HttpHeadersTransformationMiddlewareTests : UnitTest\n{\n    private readonly Mock<IHttpContextRequestHeaderReplacer> _preReplacer;\n    private readonly Mock<IHttpResponseHeaderReplacer> _postReplacer;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly HttpHeadersTransformationMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly Mock<IAddHeadersToResponse> _addHeadersToResponse;\n    private readonly Mock<IAddHeadersToRequest> _addHeadersToRequest;\n    private readonly DefaultHttpContext _httpContext;\n\n    public HttpHeadersTransformationMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _preReplacer = new Mock<IHttpContextRequestHeaderReplacer>();\n        _postReplacer = new Mock<IHttpResponseHeaderReplacer>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<AuthorizationMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _addHeadersToResponse = new Mock<IAddHeadersToResponse>();\n        _addHeadersToRequest = new Mock<IAddHeadersToRequest>();\n        _middleware = new HttpHeadersTransformationMiddleware(\n            _next, _loggerFactory.Object, _preReplacer.Object,\n            _postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_pre_and_post_header_transforms()\n    {\n        // Arrange\n        GivenTheFollowingRequest();\n        GivenTheDownstreamRequestIs();\n        GivenTheRouteHasPreFindAndReplaceSetUp();\n        GivenTheHttpResponseMessageIs();\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly();\n        ThenAddHeadersToRequestIsCalledCorrectly();\n        ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly();\n        ThenAddHeadersToResponseIsCalledCorrectly();\n    }\n\n    private void ThenAddHeadersToResponseIsCalledCorrectly()\n    {\n        _addHeadersToResponse\n            .Verify(x => x.Add(_httpContext.Items.DownstreamRoute().AddHeadersToDownstream, _httpContext.Items.DownstreamResponse()), Times.Once);\n    }\n\n    private void ThenAddHeadersToRequestIsCalledCorrectly()\n    {\n        _addHeadersToRequest\n            .Verify(x => x.SetHeadersOnDownstreamRequest(_httpContext.Items.DownstreamRoute().AddHeadersToUpstream, _httpContext), Times.Once);\n    }\n\n    private void GivenTheDownstreamRequestIs()\n    {\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\n    }\n\n    private void GivenTheHttpResponseMessageIs()\n    {\n        _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage()));\n    }\n\n    private void GivenTheRouteHasPreFindAndReplaceSetUp()\n    {\n        var fAndRs = new List<HeaderFindAndReplace>();\n        var dRoute = new DownstreamRouteBuilder()\n            .WithUpstreamHeaderFindAndReplace(fAndRs)\n            .WithDownstreamHeaderFindAndReplace(fAndRs)\n            .Build();\n        var route = new Route(dRoute);\n        var dR = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(null, route);\n\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(dR.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(dR.Route.DownstreamRoute[0]);\n    }\n\n    private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()\n    {\n        _preReplacer.Verify(x => x.Replace(It.IsAny<HttpContext>(), It.IsAny<List<HeaderFindAndReplace>>()), Times.Once);\n    }\n\n    private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()\n    {\n        _postReplacer.Verify(x => x.Replace(It.IsAny<HttpContext>(), It.IsAny<List<HeaderFindAndReplace>>()), Times.Once);\n    }\n\n    private void GivenTheFollowingRequest()\n    {\n        _httpContext.Request.Headers.Append(\"test\", \"test\");\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Headers;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Headers;\n\npublic class HttpResponseHeaderReplacerTests : UnitTest\n{\n    private DownstreamResponse _response;\n    private readonly Placeholders _placeholders;\n    private readonly HttpResponseHeaderReplacer _replacer;\n    private List<HeaderFindAndReplace> _headerFindAndReplaces;\n    private Response _result;\n    private DownstreamRequest _request;\n    private readonly Mock<IBaseUrlFinder> _finder;\n    private readonly Mock<IRequestScopedDataRepository> _repo;\n    private readonly Mock<IHttpContextAccessor> _accessor;\n    /*private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;*/\n\n    public HttpResponseHeaderReplacerTests()\n    {\n        _repo = new Mock<IRequestScopedDataRepository>();\n        _finder = new Mock<IBaseUrlFinder>();\n        _accessor = new Mock<IHttpContextAccessor>();\n\n        //_loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _placeholders = new Placeholders(_finder.Object, _repo.Object, _accessor.Object/*,_loggerFactory.Object*/);\n        _replacer = new HttpResponseHeaderReplacer(_placeholders);\n    }\n\n    [Fact]\n    public void Should_replace_headers()\n    {\n        // Arrange\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"test\", new List<string> {\"test\"}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace> { new(\"test\", \"test\", \"chiken\", 0) };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeadersAreReplaced();\n    }\n\n    [Fact]\n    public void Should_not_replace_headers()\n    {\n        // Arrange\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"test\", new List<string> {\"test\"}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>();\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeadersAreNotReplaced();\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_with_ocelot_base_url()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com/\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com/\");\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_with_ocelot_base_url_with_port()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com/\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com:123/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com:123/\");\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_with_ocelot_base_url_and_path()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com/test/product\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com/test/product\");\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_with_ocelot_base_url_with_path_and_port()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com/test/product\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com:123/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com:123/test/product\");\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_and_port_with_ocelot_base_url()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com:123/test/product\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com/test/product\");\n    }\n\n    [Fact]\n    public void Should_replace_downstream_base_url_and_port_with_ocelot_base_url_and_port()\n    {\n        // Arrange\n        const string downstreamUrl = \"http://downstream.com:123/test/product\";\n        _request = new DownstreamRequest(new(HttpMethod.Get, \"http://test.com\") { RequestUri = new Uri(downstreamUrl) });\n        _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Location\", new List<string> {downstreamUrl}),\n            }, string.Empty);\n        _headerFindAndReplaces = new List<HeaderFindAndReplace>\n        {\n            new(\"Location\", \"{DownstreamBaseUrl}\", \"http://ocelot.com:321/\", 0),\n        };\n\n        // Act\n        WhenICallTheReplacer();\n\n        // Assert\n        ThenTheHeaderShouldBe(\"Location\", \"http://ocelot.com:321/test/product\");    \n    }\n\n    private void ThenTheHeadersAreNotReplaced()\n    {\n        _result.ShouldBeOfType<OkResponse>();\n        foreach (var f in _headerFindAndReplaces)\n        {\n            var values = _response.Headers.First(x => x.Key == f.Key);\n            values.Values.ToList()[f.Index].ShouldBe(\"test\");\n        }\n    }\n\n    private void WhenICallTheReplacer()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Items.UpsertDownstreamResponse(_response);\n        httpContext.Items.UpsertDownstreamRequest(_request);\n\n        _result = _replacer.Replace(httpContext, _headerFindAndReplaces);\n    }\n\n    private void ThenTheHeaderShouldBe(string key, string value)\n    {\n        var test = _response.Headers.First(x => x.Key == key);\n        test.Values.First().ShouldBe(value);\n    }\n\n    private void ThenTheHeadersAreReplaced()\n    {\n        _result.ShouldBeOfType<OkResponse>();\n        foreach (var f in _headerFindAndReplaces)\n        {\n            var values = _response.Headers.First(x => x.Key == f.Key);\n            values.Values.ToList()[f.Index].ShouldBe(f.Replace);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs",
    "content": "﻿using Ocelot.Middleware;\r\nusing Ocelot.Headers;\n\r\nnamespace Ocelot.UnitTests.Headers;\r\n\r\npublic class RemoveHeadersTests : UnitTest\r\n{\r\n    private readonly RemoveOutputHeaders _removeOutputHeaders = new();\r\n\r\n    [Fact]\r\n    public void Should_remove_header()\r\n    {\r\n        // Arrange\r\n        var headers = new List<Header>\r\n        {\r\n            new(\"Transfer-Encoding\", new List<string> {\"chunked\"}),\r\n        };\r\n\r\n        // Act\r\n        var result = _removeOutputHeaders.Remove(headers);\r\n\r\n        // Assert\r\n        result.IsError.ShouldBeFalse();\r\n        headers.ShouldNotContain(x => x.Key == \"Transfer-Encoding\");\r\n        headers.ShouldNotContain(x => x.Key == \"transfer-encoding\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs",
    "content": "﻿using Ocelot.Errors;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\r\nnamespace Ocelot.UnitTests.Infrastructure;\r\n\r\n/// <summary>\r\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-headers\">Claims to Headers</see>.\r\n/// </summary>\r\n[Trait(\"Commit\", \"84256e7\")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea\r\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\r\npublic class ClaimParserTests : UnitTest\r\n{\r\n    private readonly ClaimsParser _claimsParser;\r\n    private readonly List<Claim> _claims;\r\n\r\n    public ClaimParserTests()\r\n    {\r\n        _claimsParser = new();\r\n        _claims = new();\r\n    }\r\n\r\n    [Fact]\r\n    public void Can_parse_claims_dictionary_access_string_returning_value_to_function()\r\n    {\r\n        // Arrange\r\n        const string key = \"CustomerId\";\r\n        _claims.Add(new Claim(key, \"1234\"));\r\n\r\n        // Act\r\n        var result = _claimsParser.GetValue(_claims, key, default, default);\r\n\r\n        // Assert\r\n        ThenTheResultIs(result, new OkResponse<string>(\"1234\"));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_error_response_when_cannot_find_requested_claim()\r\n    {\r\n        // Arrange\r\n        const string key = \"CustomerId\";\r\n        _claims.Add(new Claim(\"BallsId\", \"1234\"));\r\n\r\n        // Act\r\n        var result = _claimsParser.GetValue(_claims, key, default, default);\r\n\r\n        // Assert\r\n        ThenTheResultIs(result, new ErrorResponse<string>(new List<Error>\r\n        {\r\n            new CannotFindClaimError($\"Cannot find claim for key: {key}\"),\r\n        }));\r\n    }\r\n\r\n    [Fact]\r\n    public void Can_parse_claims_dictionary_access_string_using_delimiter_and_retuning_at_correct_index()\r\n    {\r\n        // Arrange\r\n        const string key = \"Subject\";\r\n        _claims.Add(new Claim(\"Subject\", \"registered|4321\"));\r\n\r\n        // Act\r\n        var result = _claimsParser.GetValue(_claims, key, \"|\", 1);\r\n\r\n        // Assert\r\n        ThenTheResultIs(result, new OkResponse<string>(\"4321\"));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_error_response_if_index_too_large()\r\n    {\r\n        // Arrange\r\n        const string key = \"Subject\";\r\n        const string delimiter = \"|\";\r\n        const int index = 24;\r\n        _claims.Add(new Claim(\"Subject\", \"registered|4321\"));\r\n\r\n        // Act\r\n        var result = _claimsParser.GetValue(_claims, key, delimiter, index);\r\n\r\n        // Assert\r\n        ThenTheResultIs(result, new ErrorResponse<string>(new List<Error>\r\n        {\r\n            new CannotFindClaimError($\"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}\"),\r\n        }));\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_error_response_if_index_too_small()\r\n    {\r\n        // Arrange\r\n        const string key = \"Subject\";\r\n        const string delimiter = \"|\";\r\n        const int index = -1;\r\n        _claims.Add(new Claim(\"Subject\", \"registered|4321\"));\r\n\r\n        // Act\r\n        var result = _claimsParser.GetValue(_claims, key, delimiter, index);\r\n\r\n        // Assert\r\n        ThenTheResultIs(result, new ErrorResponse<string>(new List<Error>\r\n        {\r\n            new CannotFindClaimError($\"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}\"),\r\n        }));\r\n    }\r\n\r\n    private static void ThenTheResultIs(Response<string> actual, Response<string> expected)\r\n    {\r\n        actual.Data.ShouldBe(expected.Data);\r\n        actual.IsError.ShouldBe(expected.IsError);\r\n    }\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Ocelot.Infrastructure;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Infrastructure;\n\npublic class ConfigAwarePlaceholdersTests\n{\n    private readonly ConfigAwarePlaceholders _placeholders;\n    private readonly Mock<IPlaceholders> _basePlaceholders;\n\n    public ConfigAwarePlaceholdersTests()\n    {\n        var configurationBuilder = new ConfigurationBuilder();\n        configurationBuilder.AddJsonFile(\"appsettings.json\");\n        var configuration = configurationBuilder.Build();\n\n        _basePlaceholders = new Mock<IPlaceholders>();\n        _placeholders = new ConfigAwarePlaceholders(configuration, _basePlaceholders.Object);\n    }\n\n    [Fact]\n    public void Should_return_value_from_underlying_placeholders()\n    {\n        // Arrange\n        var baseUrl = \"http://www.bbc.co.uk\";\n        const string key = \"{BaseUrl}\";\n        _basePlaceholders.Setup(x => x.Get(key)).Returns(new OkResponse<string>(baseUrl));\n\n        // Act\n        var result = _placeholders.Get(key);\n\n        // Assert\n        result.Data.ShouldBe(baseUrl);\n    }\n\n    [Fact]\n    public void Should_return_value_from_config_with_same_name_as_placeholder_if_underlying_placeholder_not_found()\n    {\n        // Arrange\n        const string expected = \"http://foo-bar.co.uk\";\n        const string key = \"{BaseUrl}\";\n        _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse<string>(new FakeError()));\n\n        // Act\n        var result = _placeholders.Get(key);\n\n        // Assert\n        result.Data.ShouldBe(expected);\n    }\n\n    [Theory]\n    [InlineData(\"{TestConfig}\")]\n    [InlineData(\"{TestConfigNested:Child}\")]\n    public void Should_return_value_from_config(string key)\n    {\n        // Arrange\n        const string expected = \"foo\";\n        _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse<string>(new FakeError()));\n\n        // Act\n        var result = _placeholders.Get(key);\n\n        // Assert\n        result.Data.ShouldBe(expected);\n    }\n\n    [Fact]\n    public void Should_call_underyling_when_added()\n    {\n        // Arrange\n        const string key = \"{Test}\";\n        Func<Response<string>> func = () => new OkResponse<string>(\"test)\");\n\n        // Act\n        _placeholders.Add(key, func);\n\n        // Assert\n        _basePlaceholders.Verify(p => p.Add(key, func), Times.Once);\n    }\n\n    [Fact]\n    public void Should_call_underyling_when_removed()\n    {\n        // Arrange\n        const string key = \"{Test}\";\n\n        // Act\n        _placeholders.Remove(key);\n\n        // Assert\n        _basePlaceholders.Verify(p => p.Remove(key), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/ErrorListExtensionsTests.cs",
    "content": "﻿using Ocelot.Errors;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class ErrorListExtensionsTests\n{\n    private static readonly string NL = Environment.NewLine;\n\n    [Fact]\n    public void ToErrorString_ShouldReturnEmptyString_WhenListIsEmpty()\n    {\n        // Arrange\n        var errors = new List<Error>();\n\n        // Act\n        var result = errors.ToErrorString();\n\n        // Assert\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public void ToErrorString_ShouldReturnSingleError_WhenListHasOneItem()\n    {\n        // Arrange\n        var errors = new List<Error> { new CannotAddPlaceholderError(\"First error\") };\n\n        // Act\n        var result = errors.ToErrorString();\n\n        // Assert\n        Assert.Equal(\"CannotAddPlaceholderError: First error\", result);\n    }\n\n    [Fact]\n    public void ToErrorString_ShouldJoinMultipleErrors_WithNewLine()\n    {\n        // Arrange\n        var errors = new List<Error>\n            {\n                new CannotAddPlaceholderError(\"First\"),\n                new CannotAddPlaceholderError(\"Second\"),\n                new CannotAddPlaceholderError(\"Third\"),\n            };\n\n        // Act\n        var result = errors.ToErrorString();\n\n        // Assert\n        var expected = $\"CannotAddPlaceholderError: First{NL}CannotAddPlaceholderError: Second{NL}CannotAddPlaceholderError: Third\";\n        Assert.Equal(expected, result);\n    }\n\n    [Fact]\n    public void ToErrorString_ShouldInsertNewLineBefore_WhenBeforeIsTrue()\n    {\n        // Arrange\n        var errors = new List<Error> { new CannotAddPlaceholderError(\"First\") };\n\n        // Act\n        var result = errors.ToErrorString(before: true);\n\n        // Assert\n        var expected = $\"{NL}CannotAddPlaceholderError: First\";\n        Assert.Equal(expected, result);\n    }\n\n    [Fact]\n    public void ToErrorString_ShouldInsertNewLineAfter_WhenAfterIsTrue()\n    {\n        // Arrange\n        var errors = new List<Error> { new CannotAddPlaceholderError(\"First\") };\n\n        // Act\n        var result = errors.ToErrorString(after: true);\n\n        // Assert\n        var expected = $\"CannotAddPlaceholderError: First{NL}\";\n        Assert.Equal(expected, result);\n    }\n\n    [Fact]\n    public void ToErrorString_ShouldInsertNewLineBeforeAndAfter_WhenBothFlagsTrue()\n    {\n        // Arrange\n        var errors = new List<Error> { new CannotAddPlaceholderError(\"First\") };\n\n        // Act\n        var result = errors.ToErrorString(before: true, after: true);\n\n        // Assert\n        var expected = $\"{NL}CannotAddPlaceholderError: First{NL}\";\n        Assert.Equal(expected, result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/HttpContextExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class HttpContextExtensionsTests\n{\n    [Fact]\n    public void IsOptionsMethod_ShouldReturnTrue_ForOptionsVerb()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = HttpMethod.Options.Method; // \"OPTIONS\"\n\n        // Act\n        var result = context.IsOptionsMethod();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"GET\")]\n    [InlineData(\"POST\")]\n    [InlineData(\"PUT\")]\n    [InlineData(\"DELETE\")]\n    [InlineData(\"PATCH\")]\n    public void IsOptionsMethod_ShouldReturnFalse_ForNonOptionsVerbs(string method)\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = method;\n\n        // Act\n        var result = context.IsOptionsMethod();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsOptionsMethod_ShouldBeCaseInsensitive()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = \"options\"; // lowercase\n\n        // Act\n        var result = context.IsOptionsMethod();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void IsOptionsMethod_ShouldReturnFalse_WhenMethodIsEmpty()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = string.Empty;\n\n        // Act\n        var result = context.IsOptionsMethod();\n\n        // Assert\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/HttpRequestExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class HttpRequestExtensionsTests\n{\n    [Fact]\n    public void IsOptionsMethod_ShouldReturnTrue_ForOptionsVerb()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = HttpMethod.Options.Method; // \"OPTIONS\"\n\n        // Act\n        var result = context.Request.IsOptionsMethod();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"GET\")]\n    [InlineData(\"POST\")]\n    [InlineData(\"PUT\")]\n    [InlineData(\"DELETE\")]\n    public void IsOptionsMethod_ShouldReturnFalse_ForNonOptionsVerbs(string method)\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = method;\n\n        // Act\n        var result = context.Request.IsOptionsMethod();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsOptionsMethod_ShouldBeCaseInsensitive()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = \"options\"; // lowercase\n\n        // Act\n        var result = context.Request.IsOptionsMethod();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void IsOptionsMethod_ShouldReturnFalse_WhenMethodIsEmpty()\n    {\n        // Arrange\n        var context = new DefaultHttpContext();\n        context.Request.Method = string.Empty;\n\n        // Act\n        var result = context.Request.IsOptionsMethod();\n\n        // Assert\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/IEnumerableExtensionsTests.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class IEnumerableExtensionsTests\n{\n    [Fact]\n    public void ToHttpMethods_ShouldReturnEmptyHashSet_WhenCollectionIsNull()\n    {\n        // Arrange\n        IEnumerable<string> collection = null;\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void ToHttpMethods_ShouldReturnEmptyHashSet_WhenCollectionIsEmpty()\n    {\n        // Arrange\n        var collection = Enumerable.Empty<string>();\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [InlineData(\"GET\")]\n    [InlineData(\"POST\")]\n    [InlineData(\"PUT\")]\n    [InlineData(\"DELETE\")]\n    public void ToHttpMethods_ShouldReturnCorrectHttpMethod(string verb)\n    {\n        // Arrange\n        var collection = new[] { verb };\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(verb, result.First().Method);\n    }\n\n    [Fact]\n    public void ToHttpMethods_ShouldTrimWhitespace()\n    {\n        // Arrange\n        var collection = new[] { \"  GET  \" };\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"GET\", result.First().Method);\n    }\n\n    [Fact]\n    public void ToHttpMethods_ShouldRemoveDuplicates()\n    {\n        // Arrange\n        var collection = new[] { \"GET\", \"GET\", \"POST\" };\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.Equal(2, result.Count);\n        Assert.Contains(result, m => m.Method == \"GET\");\n        Assert.Contains(result, m => m.Method == \"POST\");\n    }\n\n    [Fact]\n    public void ToHttpMethods_ShouldHandleMixedVerbs()\n    {\n        // Arrange\n        var collection = new[] { \"GET\", \"POST\", \"PUT\", \"DELETE\", \"DELETE\" };\n\n        // Act\n        var result = collection.ToHttpMethods();\n\n        // Assert\n        Assert.Equal(4, result.Count);\n        Assert.Contains(result, m => m.Method == \"GET\");\n        Assert.Contains(result, m => m.Method == \"POST\");\n        Assert.Contains(result, m => m.Method == \"PUT\");\n        Assert.Contains(result, m => m.Method == \"DELETE\");\n    }\n\n    // ---------------------------\n    // Tests for NotNull<T>\n    // ---------------------------\n    [Fact]\n    public void NotNull_ShouldReturnEmptyEnumerable_WhenCollectionIsNull()\n    {\n        // Arrange\n        IEnumerable<int> collection = null;\n\n        // Act\n        var result = collection.NotNull();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void NotNull_ShouldReturnSameCollection_WhenNotNull()\n    {\n        // Arrange\n        var collection = new[] { 1, 2, 3 };\n\n        // Act\n        var result = collection.NotNull();\n\n        // Assert\n        Assert.Same(collection, result);\n    }\n\n    // ---------------------------\n    // Tests for Csv\n    // ---------------------------\n    [Fact]\n    public void Csv_ShouldReturnEmptyString_WhenCollectionIsNull()\n    {\n        // Arrange\n        IEnumerable<string> values = null;\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public void Csv_ShouldReturnEmptyString_WhenCollectionIsEmpty()\n    {\n        // Arrange\n        var values = Enumerable.Empty<string>();\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public void Csv_ShouldJoinValuesWithComma()\n    {\n        // Arrange\n        var values = new[] { \"one\", \"two\", \"three\" };\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        Assert.Equal(\"one,two,three\", result);\n    }\n\n    [Fact]\n    public void Csv_ShouldHandleSingleValue()\n    {\n        // Arrange\n        var values = new[] { \"only\" };\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        Assert.Equal(\"only\", result);\n    }\n\n    [Fact]\n    public void Csv_ShouldPreserveEmptyStrings()\n    {\n        // Arrange\n        var values = new[] { \"a\", \"\", \"b\" };\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        Assert.Equal(\"a,,b\", result);\n    }\n\n    [Fact]\n    public void Csv_ShouldTrimWhitespaceInsideValues()\n    {\n        // Arrange\n        var values = new[] { \" a \", \" b \", \"c\" };\n\n        // Act\n        var result = values.Csv();\n\n        // Assert\n        // Note: Csv does not trim, so whitespace is preserved\n        Assert.Equal(\" a , b ,c\", result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/Int32ExtensionsTests.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class Int32ExtensionsTests\n{\n    // ---------------------------\n    // Tests for Ensure\n    // ---------------------------\n    [Theory]\n    [InlineData(-5, 0, 0)]// below default low\n    [InlineData(5, 0, 5)] // above default low\n    [InlineData(3, 2, 3)] // above custom low\n    [InlineData(1, 5, 5)] // below custom low\n    [InlineData(5, 5, 5)] // equal to low\n    public void Ensure_ShouldReturnExpectedValue(int value, int low, int expected)\n    {\n        // Act\n        var result = value.Ensure(low);\n\n        // Assert\n        Assert.True(result >= low);\n        Assert.Equal(expected, result);\n    }\n\n    // ---------------------------\n    // Tests for Positive (int)\n    // ---------------------------\n    [Theory]\n    [InlineData(-10, 1)]// negative becomes 1\n    [InlineData(0, 1)] // zero becomes 1\n    [InlineData(5, 5)] // positive stays same\n    public void Positive_Int_ShouldReturnPositiveValue(int value, int expected)\n    {\n        // Act\n        var result = value.Positive();\n\n        // Assert\n        Assert.True(result > 0);\n        Assert.Equal(expected, result);\n    }\n\n    // ---------------------------\n    // Tests for Positive (int?)\n    // ---------------------------\n    [Fact]\n    public void Positive_NullableInt_ShouldReturnNull_WhenNoValue()\n    {\n        // Arrange\n        int? value = null;\n\n        // Act\n        var result = value.Positive();\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [InlineData(-5, 1, 1)] // negative becomes default\n    [InlineData(-5, 99, 99)] // negative becomes custom default\n    [InlineData(0, 1, 1)] // zero becomes default\n    [InlineData(10, 1, 10)] // positive stays same\n    public void Positive_NullableInt_ShouldReturnPositiveValue(int? value, int toDefault, int expected)\n    {\n        // Act\n        var result = value.Positive(toDefault);\n\n        // Assert\n        Assert.True(result > 0);\n        Assert.Equal(expected, result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/StringBuilderExtensionsTests.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class StringBuilderExtensionsTests\n{\n    [Fact]\n    public void AppendNext_ShouldAppendWithoutSeparator_WhenBuilderIsEmpty()\n    {\n        // Arrange\n        var sb = new StringBuilder();\n\n        // Act\n        sb.AppendNext(\"first\");\n\n        // Assert\n        Assert.Equal(\"first\", sb.ToString());\n    }\n\n    [Fact]\n    public void AppendNext_ShouldAppendWithSeparator_WhenBuilderHasContent()\n    {\n        // Arrange\n        var sb = new StringBuilder(\"first\");\n\n        // Act\n        sb.AppendNext(\"second\");\n\n        // Assert\n        Assert.Equal(\"first,second\", sb.ToString());\n    }\n\n    [Fact]\n    public void AppendNext_ShouldUseCustomSeparator()\n    {\n        // Arrange\n        var sb = new StringBuilder(\"first\");\n\n        // Act\n        sb.AppendNext(\"second\", ';');\n\n        // Assert\n        Assert.Equal(\"first;second\", sb.ToString());\n    }\n\n    [Fact]\n    public void AppendNext_ShouldAllowMultipleAppends()\n    {\n        // Arrange\n        var sb = new StringBuilder();\n\n        // Act\n        sb.AppendNext(\"one\");\n        sb.AppendNext(\"two\");\n        sb.AppendNext(\"three\");\n\n        // Assert\n        Assert.Equal(\"one,two,three\", sb.ToString());\n    }\n\n    [Fact]\n    public void AppendNext_ShouldHandleEmptyStringAsNext()\n    {\n        // Arrange\n        var sb = new StringBuilder(\"first\");\n\n        // Act\n        sb.AppendNext(\"\");\n\n        // Assert\n        Assert.Equal(\"first,\", sb.ToString());\n    }\n\n    [Fact]\n    public void AppendNext_ShouldReturnSameBuilderInstance()\n    {\n        // Arrange\n        var sb = new StringBuilder();\n\n        // Act\n        var result = sb.AppendNext(\"test\");\n\n        // Assert\n        Assert.Same(sb, result);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/Extensions/StringExtensionsTests.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\n\nnamespace Ocelot.UnitTests.Infrastructure.Extensions;\n\npublic class StringExtensionsTests\n{\n    [Fact]\n    public void TrimPrefix_ArgsCheck_ReturnedSource()\n    {\n        ((string)null).TrimPrefix(\"/\").ShouldBeNull();\n        \"x\".TrimPrefix(null).ShouldBe(\"x\");\n        \"x\".TrimPrefix(string.Empty).ShouldBe(\"x\");\n    }\n\n    [Fact]\n    public void TrimPrefix_HasPrefix_HappyPath()\n    {\n        \"/string\".TrimPrefix(\"/\").ShouldBe(\"string\");\n        \"///string\".TrimPrefix(\"/\").ShouldBe(\"string\");\n        \"ABABstring\".TrimPrefix(\"AB\").ShouldBe(\"string\");\n    }\n\n    [Fact]\n    public void LastCharAsForwardSlash_HappyPath()\n    {\n        \"string\".LastCharAsForwardSlash().ShouldBe(\"string/\");\n        \"string/\".LastCharAsForwardSlash().ShouldBe(\"string/\");\n    }\n\n    [Theory]\n    [InlineData(0, \"s\")]\n    [InlineData(1, \"\")]\n    [InlineData(2, \"s\")]\n    public void Plural_Int32(int count, string expected)\n    {\n        var actual = count.Plural();\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(\"item\", 0, \"items\")]\n    [InlineData(\"item\", 1, \"item\")]\n    [InlineData(\"item\", 2, \"items\")]\n    public void Plural_ThisString(string source, int count, string expected)\n    {\n        var actual = source.Plural(count);\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(null, true)]\n    [InlineData(\"\", true)]\n    [InlineData(\" \", true)]\n    [InlineData(\"x\", false)]\n    public void IsEmpty(string str, bool expected)\n    {\n        bool actual = str.IsEmpty();\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    [InlineData(\" \", false)]\n    [InlineData(\"x\", true)]\n    public void IsNotEmpty(string str, bool expected)\n    {\n        bool actual = str.IsNotEmpty();\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(null, \"def\", \"def\")]\n    [InlineData(\"\", \"def\", \"def\")]\n    [InlineData(\" \", \"def\", \"def\")]\n    [InlineData(\"x\", \"def\", \"x\")]\n    public void IfEmpty(string str, string def, string expected)\n    {\n        var actual = str.IfEmpty(def);\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Infrastructure;\n\npublic class HttpDataRepositoryTests : UnitTest\n{\n    private readonly DefaultHttpContext _httpContext;\n    private readonly IHttpContextAccessor _httpContextAccessor;\n    private readonly HttpDataRepository _repository;\n    private object _result;\n\n    public HttpDataRepositoryTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _httpContextAccessor = new HttpContextAccessor { HttpContext = _httpContext };\n        _repository = new HttpDataRepository(_httpContextAccessor);\n    }\n\n    /*\n    TODO - Additional tests -> Type mistmatch aka Add string, request int\n    TODO - Additional tests -> HttpContent null. This should never happen\n    */\n    [Fact]\n    public void Get_returns_correct_key_from_http_context()\n    {\n        // Arrange\n        _httpContext.Items.Add(\"key\", \"string\");\n\n        // Act\n        _result = _repository.Get<string>(\"key\");\n\n        // Assert\n        ThenTheResultIsAnOkResponse<string>(\"string\");\n    }\n\n    [Fact]\n    public void Get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null\n    {\n        // Arrange\n        _httpContext.Items.Add(\"key\", \"string\");\n\n        // Act\n        _result = _repository.Get<string>(\"keyDoesNotExist\");\n\n        // Assert\n        ThenTheResultIsAnErrorReposnse<string>();\n    }\n\n    [Fact]\n    public void Should_update()\n    {\n        // Arrange\n        _httpContext.Items.Add(\"key\", \"string\");\n        _repository.Update<string>(\"key\", \"new string\");\n\n        // Act\n        _result = _repository.Get<string>(\"key\");\n\n        // Assert\n        ThenTheResultIsAnOkResponse<string>(\"new string\");\n    }\n\n    private void ThenTheResultIsAnErrorReposnse<T>()\n    {\n        _result.ShouldBeOfType<ErrorResponse<T>>();\n        ((ErrorResponse<T>)_result).Data.ShouldBe(default);\n        ((ErrorResponse<T>)_result).IsError.ShouldBe(true);\n        ((ErrorResponse<T>)_result).Errors.ShouldHaveSingleItem()\n            .ShouldBeOfType<CannotFindDataError>()\n            .Message.ShouldStartWith(\"Unable to find data for key: \");\n    }\n\n    private void ThenTheResultIsAnOkResponse<T>(object resultValue)\n    {\n        _result.ShouldBeOfType<OkResponse<T>>();\n        ((OkResponse<T>)_result).Data.ShouldBe(resultValue);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs",
    "content": "using Ocelot.Infrastructure;\n\nnamespace Ocelot.UnitTests.Infrastructure;\n\npublic class InMemoryBusTests\n{\n    private readonly InMemoryBus<object> _bus = new();\n\n    [Fact]\n    public async Task Should_publish_with_delay()\n    {\n        // Arrange\n        var called = false;\n        _bus.Subscribe(x => called = true);\n\n        // Act\n        _bus.Publish(new object(), 1);\n        await Task.Delay(100, TestContext.Current.CancellationToken);\n\n        // Assert\n        called.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_not_be_publish_yet_as_no_delay_in_caller()\n    {\n        // Arrange\n        var called = false;\n        _bus.Subscribe(x => called = true);\n\n        // Act\n        _bus.Publish(new object(), 1);\n\n        // Assert\n        called.ShouldBeFalse();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Infrastructure;\n\npublic class PlaceholdersTests\n{\n    private readonly Mock<IBaseUrlFinder> _finder;\n    private readonly Mock<IRequestScopedDataRepository> _repo;\n    private readonly Mock<IHttpContextAccessor> _accessor;\n    /*private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;*/\n    private readonly Placeholders _placeholders;\n\n    public PlaceholdersTests()\n    {\n        _finder = new Mock<IBaseUrlFinder>();\n        _repo = new Mock<IRequestScopedDataRepository>();\n        _accessor = new Mock<IHttpContextAccessor>();\n\n        //_loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _placeholders = new Placeholders(_finder.Object, _repo.Object, _accessor.Object);\n    }\n\n    [Fact]\n    public void Should_return_base_url()\n    {\n        // Arrange\n        var baseUrl = \"http://www.bbc.co.uk\";\n        _finder.Setup(x => x.Find()).Returns(baseUrl);\n\n        // Act\n        var result = _placeholders.Get(\"{BaseUrl}\");\n\n        // Assert\n        result.Data.ShouldBe(baseUrl);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"623\")] // https://github.com/ThreeMammals/Ocelot/issues/623\n    [Trait(\"PR\", \"632\")] // https://github.com/ThreeMammals/Ocelot/pull/632\n    public void Should_return_remote_ip_address()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext { Connection = { RemoteIpAddress = IPAddress.Any } };\n        _accessor.Setup(x => x.HttpContext).Returns(httpContext);\n\n        // Act\n        var result = _placeholders.Get(\"{RemoteIpAddress}\");\n\n        // Assert\n        result.Data.ShouldBe(httpContext.Connection.RemoteIpAddress.ToString());\n    }\n\n    [Fact]\n    public void Should_return_key_does_not_exist()\n    {\n        // Arrange, Act\n        var result = _placeholders.Get(\"{Test}\");\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].Message.ShouldBe(\"Unable to find placeholder called {Test}\");\n    }\n\n    [Fact]\n    public void Should_return_downstream_base_url_when_port_is_not_80_or_443()\n    {\n        // Arrange\n        var httpRequest = new HttpRequestMessage\n        {\n            RequestUri = new(\"http://www.bbc.co.uk\"),\n        };\n        var request = new DownstreamRequest(httpRequest);\n\n        // Act\n        var result = _placeholders.Get(\"{DownstreamBaseUrl}\", request);\n\n        // Assert\n        result.Data.ShouldBe(\"http://www.bbc.co.uk/\");\n    }\n\n    [Fact]\n    public void Should_return_downstream_base_url_when_port_is_80_or_443()\n    {\n        // Arrange\n        var httpRequest = new HttpRequestMessage\n        {\n            RequestUri = new(\"http://www.bbc.co.uk:123\"),\n        };\n        var request = new DownstreamRequest(httpRequest);\n\n        // Act\n        var result = _placeholders.Get(\"{DownstreamBaseUrl}\", request);\n\n        // Assert\n        result.Data.ShouldBe(\"http://www.bbc.co.uk:123/\");\n    }\n\n    [Fact]\n    public void Should_return_key_does_not_exist_for_http_request_message()\n    {\n        // Arrange\n        var request = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://west.com\"));\n\n        // Act\n        var result = _placeholders.Get(\"{Test}\", request);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].Message.ShouldBe(\"Unable to find placeholder called {Test}\");\n    }\n\n    [Fact]\n    public void Should_return_trace_id()\n    {\n        // Arrange\n        var traceId = \"123\";\n        _repo.Setup(x => x.Get<string>(\"TraceId\")).Returns(new OkResponse<string>(traceId));\n\n        // Act\n        var result = _placeholders.Get(\"{TraceId}\");\n\n        // Assert\n        result.Data.ShouldBe(traceId);\n    }\n\n    [Fact]\n    public void Should_return_ok_when_added()\n    {\n        // Arrange, Act\n        var result = _placeholders.Add(\"{Test}\", () => new OkResponse<string>(\"test\"));\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_return_ok_when_removed()\n    {\n        // Arrange\n        var result = _placeholders.Add(\"{Test}\", () => new OkResponse<string>(\"test\"));\n\n        // Act\n        result = _placeholders.Remove(\"{Test}\");\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n    }\n\n    [Fact]\n    public void Should_return_error_when_added()\n    {\n        // Arrange\n        var result = _placeholders.Add(\"{Test}\", () => new OkResponse<string>(\"test\"));\n\n        // Act\n        result = _placeholders.Add(\"{Test}\", () => new OkResponse<string>(\"test\"));\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].Message.ShouldBe(\"Unable to add placeholder: {Test}, placeholder already exists\");\n    }\n\n    [Fact]\n    public void Should_return_error_when_removed()\n    {\n        // Arrange, Act\n        var result = _placeholders.Remove(\"{Test}\");\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].Message.ShouldBe(\"Unable to remove placeholder: {Test}, placeholder does not exists\");\n    }\n\n    [Fact]\n    public void Should_return_upstreamHost()\n    {\n        // Arrange\n        var upstreamHost = \"UpstreamHostA\";\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Headers.Append(\"Host\", upstreamHost);\n        _accessor.Setup(x => x.HttpContext).Returns(httpContext);\n\n        // Act\n        var result = _placeholders.Get(\"{UpstreamHost}\");\n\n        // Assert\n        result.Data.ShouldBe(upstreamHost);\n    }\n\n    [Fact]\n    public void Should_return_error_when_finding_upstbecause_Host_not_set()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        _accessor.Setup(x => x.HttpContext).Returns(httpContext);\n\n        // Act\n        var result = _placeholders.Get(\"{UpstreamHost}\");\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_return_error_when_finding_upstream_host_because_exception_thrown()\n    {\n        // Arrange\n        _accessor.Setup(x => x.HttpContext).Throws(new Exception());\n\n        // Act\n        var result = _placeholders.Get(\"{UpstreamHost}\");\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Get_GetRemoteIpAddress()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext { Connection = { RemoteIpAddress = IPAddress.Any } };\n        _accessor.Setup(x => x.HttpContext).Returns(httpContext);\n\n        // Act\n        var result = _placeholders.Get(\"{RemoteIpAddress}\");\n\n        // Assert\n        result.Data.ShouldBe(httpContext.Connection.RemoteIpAddress.ToString());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs",
    "content": "using Ocelot.Authorization;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.Infrastructure;\n\npublic class ScopesAuthorizerTests : UnitTest\n{\n    private readonly ScopesAuthorizer _authorizer;\n    private readonly Mock<IClaimsParser> _parser;\n\n    public ScopesAuthorizerTests()\n    {\n        _parser = new Mock<IClaimsParser>();\n        _authorizer = new ScopesAuthorizer(_parser.Object);\n    }\n\n    [Fact]\n    public void Should_return_ok_if_no_allowed_scopes()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string>();\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    public void Should_return_ok_if_null_allowed_scopes()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = (List<string>)null;\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    public void Should_return_error_if_claims_parser_returns_error()\n    {\n        // Arrange\n        var fakeError = new FakeError();\n        var principal = new ClaimsPrincipal();\n        GivenTheParserReturns(new ErrorResponse<List<string>>(fakeError));\n        var allowedScopes = new List<string> { \"doesntmatter\" };\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));\n    }\n\n    [Fact]\n    public void Should_match_scopes_and_return_ok_result()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"someScope\" };\n        GivenTheParserReturns(new OkResponse<List<string>>(allowedScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    public void Should_not_match_scopes_and_return_error_result()\n    {\n        // Arrange\n        var fakeError = new FakeError();\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"someScope\" };\n        var userScopes = new List<string> { \"anotherScope\" };\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));\n    }\n\n    #region PR 1478\n    [Fact]\n    [Trait(\"Bug\", \"913\")] // https://github.com/ThreeMammals/Ocelot/issues/913\n    [Trait(\"PR\", \"1478\")] // https://github.com/ThreeMammals/Ocelot/pull/1478\n    public void Should_split_space_separated_scope_and_match()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"api.read\", \"api.write\" };\n        var userScopes = new List<string> { \"api.read api.write openid\" }; // Space-separated scope claim\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    public void Should_split_space_separated_scope_and_match_single_scope()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"api.write\" };\n        var userScopes = new List<string> { \"api.read api.write openid\" }; // Space-separated scope claim\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    public void Should_split_space_separated_scope_but_not_match()\n    {\n        // Arrange\n        var fakeError = new FakeError();\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"admin\" };\n        var userScopes = new List<string> { \"api.read api.write openid\" }; // Space-separated scope claim\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new ErrorResponse<bool>(fakeError));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    public void Should_handle_multiple_scope_claims_without_splitting()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"api.read\" };\n        var userScopes = new List<string> { \"api.read\", \"api.write\" }; // Multiple separate claims\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    public void Should_not_split_single_scope_without_spaces()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"api.read\" };\n        var userScopes = new List<string> { \"api.read\" }; // Single scope without spaces\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"913\")]\n    [Trait(\"PR\", \"1478\")]\n    public void Should_handle_empty_string_after_splitting()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal();\n        var allowedScopes = new List<string> { \"api.read\" };\n        var userScopes = new List<string> { \"  api.read  api.write  \" }; // Scope with extra spaces\n        GivenTheParserReturns(new OkResponse<List<string>>(userScopes));\n\n        // Act\n        var result = _authorizer.Authorize(principal, allowedScopes);\n\n        // Assert\n        ThenTheFollowingIsReturned(result, new OkResponse<bool>(true));\n    }\n    #endregion PR 1478\n\n    private void GivenTheParserReturns(Response<List<string>> response)\n    {\n        _parser.Setup(x => x.GetValuesByClaimType(It.IsAny<IEnumerable<Claim>>(), It.IsAny<string>())).Returns(response);\n    }\n\n    private static void ThenTheFollowingIsReturned(Response<bool> actual, Response<bool> expected)\n    {\n        actual.Data.ShouldBe(expected.Data);\n        actual.IsError.ShouldBe(expected.IsError);\n    }\n}\n\npublic class FakeError : Error\n{\n    public FakeError() : base(\"fake error\", OcelotErrorCode.CannotAddDataError, 404)\n    {\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/EndpointClientV1Tests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Http;\nusing KubeClient.Http.Formatters;\nusing KubeClient.Models;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"2168\")]\n[Trait(\"PR\", \"2174\")] // https://github.com/ThreeMammals/Ocelot/pull/2174\npublic class EndpointClientV1Tests\n{\n    private readonly EndPointClientV1 _endpointClient;\n    private readonly Mock<IKubeApiClient> _kubeApiClient = new();\n\n    public EndpointClientV1Tests()\n    {\n        var loggerFactory = new Mock<ILoggerFactory>();\n        loggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))\n            .Returns(new Mock<ILogger>().Object);\n        _kubeApiClient.Setup(x => x.LoggerFactory)\n            .Returns(loggerFactory.Object);\n        _endpointClient = new EndPointClientV1(_kubeApiClient.Object);\n        _kubeApiClient.Setup(x => x.ResourceClient(It.IsAny<Func<IKubeApiClient, IEndPointClient>>()))\n            .Returns(_endpointClient);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public async Task GetAsync_WhenServiceIsNullOrEmpty_ThrowsArgumentException(string serviceName)\n    {\n        // Act\n        var watchCall = () => _endpointClient.GetAsync(serviceName, null, CancellationToken.None);\n\n        // Assert\n        var e = await watchCall.ShouldThrowAsync<ArgumentException>();\n        e.ParamName.ShouldBe(nameof(serviceName));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"test-namespace\")]\n    public async Task GetAsync_KubeNamespaceChanges_HappyPath(string kubeNamespace)\n    {\n        // Arrange\n        using var client = new FakeHttpClient()\n        {\n            BaseAddress = new UriBuilder(Uri.UriSchemeHttp, \"localhost\", 1234).Uri,\n        };\n        _kubeApiClient.SetupGet(x => x.Http).Returns(client);\n        _kubeApiClient.SetupGet(x => x.DefaultNamespace).Returns(nameof(EndpointClientV1Tests));\n\n        // Act\n        var endpoints = await _endpointClient.GetAsync(\"service-XYZ\", kubeNamespace, CancellationToken.None);\n\n        // Assert\n        Assert.NotNull(endpoints);\n        Assert.Equal(nameof(GetAsync_KubeNamespaceChanges_HappyPath), endpoints.Kind);\n        var url = client.Request.RequestUri.AbsoluteUri;\n        Assert.Contains(kubeNamespace ?? nameof(EndpointClientV1Tests), url);\n        Assert.Contains(\"service-XYZ\", url);\n        client.Request.Options.TryGetValue(new(\"KubeClient.Http.Request\"), out HttpRequest request);\n        Assert.NotNull(request?.TemplateParameters);\n        Assert.True(request.TemplateParameters.ContainsKey(\"Namespace\"));\n        Assert.True(request.TemplateParameters.ContainsKey(\"ServiceName\"));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public void Watch_WhenServiceIsNullOrEmpty_ThrowsArgumentException(string serviceName)\n    {\n        // Act\n        var watchCall = () => _endpointClient.Watch(serviceName, null, CancellationToken.None);\n        \n        // Assert\n        watchCall.ShouldThrow<ArgumentException>().ParamName.ShouldBe(nameof(serviceName));\n    }\n\n    [Fact]\n    public void Watch_ProvidesObservable()\n    {\n        // Act\n        var observable = _endpointClient.Watch(\"some-service\", null, CancellationToken.None);\n        \n        // Assert\n        observable.ShouldNotBeNull();\n    }\n}\n\ninternal class FakeHttpClient : HttpClient, IDisposable\n{\n    private readonly Mock<IFormatterCollection> formatters = new();\n    private readonly Mock<IInputFormatter> formatter = new();\n    private readonly List<IDisposable> disposables = new();\n    public FakeHttpClient([CallerMemberName] string testName = null)\n    {\n        formatter.Setup(x => x.ReadAsync(It.IsAny<InputFormatterContext>(), It.IsAny<Stream>()))\n            .ReturnsAsync(() => new EndpointsV1() { Kind = testName });\n        formatters.SetupGet(x => x.Count).Returns(1);\n        formatters.Setup(x => x.FindInputFormatter(It.IsAny<InputFormatterContext>()))\n            .Returns(formatter.Object);\n    }\n\n    public new void Dispose()\n    {\n        disposables.ForEach(d => d.Dispose());\n        base.Dispose();\n    }\n\n    public HttpRequestMessage Request { get; private set; }\n    public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        Request = request;\n        HttpResponseMessage response = new()\n        {\n            StatusCode = HttpStatusCode.OK,\n            RequestMessage = new(),\n        };\n        response.RequestMessage.Properties.Add(MessageProperties.ContentFormatters, formatters.Object);\n        response.Content = new StringContent(\"Hello from \" + nameof(FakeHttpClient));\n        disposables.Add(response);\n        disposables.Add(response.Content);\n        return Task.FromResult(response);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/FakeKubeApiClientFactory.cs",
    "content": "﻿using KubeClient;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Provider.Kubernetes;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\ninternal class FakeKubeApiClientFactory : KubeApiClientFactory\n{\n    public FakeKubeApiClientFactory(ILoggerFactory logger, IOptions<KubeClientOptions> options)\n        : base(logger, options) { }\n\n    public FakeKubeApiClientFactory(ILoggerFactory logger, IOptions<KubeClientOptions> options, string serviceAccountPath)\n        : base(logger, options)\n    {\n        ServiceAccountPath = serviceAccountPath;\n    }\n\n    public FakeKubeApiClientFactory(string serviceAccountPath)\n        : base(Mock.Of<ILoggerFactory>(), Mock.Of<IOptions<KubeClientOptions>>())\n    {\n        ServiceAccountPath = serviceAccountPath;\n    }\n\n    public string ActualServiceAccountPath => ServiceAccountPath;\n\n    public KubeApiClient Actual { get; private set; }\n\n    public override KubeApiClient Get(bool usePodServiceAccount)\n    {\n        return Actual = base.Get(usePodServiceAccount);\n    }\n\n    public static async Task CreateCertificate(string crtFile)\n    {\n        var certificate = CreateCertificate();\n        byte[] certBytes = certificate.Export(X509ContentType.Cert);\n        await File.WriteAllBytesAsync(crtFile, certBytes);\n    }\n\n    public static X509Certificate2 CreateCertificate()\n    {\n        // Generate a self-signed certificate\n        using RSA rsa = RSA.Create(2048);\n        var request = new CertificateRequest(\"CN=MyCertificate\", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n\n        // Add extensions to the certificate (optional)\n        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));\n        request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));\n        request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));\n\n        return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/KubeApiClientFactoryTests.cs",
    "content": "﻿using KubeClient;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Provider.Kubernetes;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\npublic sealed class KubeApiClientFactoryTests : KubeApiClientFactoryTestsBase\n{\n    [Theory]\n    [Trait(\"Bug\", \"2299\")]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public void ServiceAccountPath_NoValue_FallbackedToDefValue(string serviceAccountPath)\n    {\n        // Arrange\n        var s = new FakeKubeApiClientFactory(serviceAccountPath);\n\n        // Act\n        var actual = s.ActualServiceAccountPath;\n\n        // Assert\n        actual.ShouldNotBeNullOrEmpty();\n        Assert.Equal(KubeClientConstants.DefaultServiceAccountPath, actual);\n    }\n}\n\n[Collection(nameof(SequentialTests))]\npublic class KubeApiClientFactorySequentialTests : KubeApiClientFactoryTestsBase\n{\n    [Fact]\n    [Trait(\"Bug\", \"2299\")]\n    public async Task Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount()\n    {\n        // Arrange\n        var serviceAccountPath = Path.Combine(AppContext.BaseDirectory, TestID);\n        var stub = new FakeKubeApiClientFactory(logger.Object, options.Object, serviceAccountPath);\n        var expectedHost = IPAddress.Loopback.ToString();\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_HOST\", expectedHost);\n        int expectedPort = PortFinder.GetRandomPort();\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", expectedPort.ToString());\n\n        folders.Add(serviceAccountPath);\n        if (!Directory.Exists(serviceAccountPath))\n        {\n            Directory.CreateDirectory(serviceAccountPath);\n        }\n\n        var path = Path.Combine(serviceAccountPath, \"namespace\");\n        await File.WriteAllTextAsync(path, nameof(Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount), TestContext.Current.CancellationToken);\n        files.Add(path);\n\n        path = Path.Combine(serviceAccountPath, \"token\");\n        await File.WriteAllTextAsync(path, TestID, TestContext.Current.CancellationToken);\n        files.Add(path);\n\n        path = Path.Combine(serviceAccountPath, \"ca.crt\");\n        await FakeKubeApiClientFactory.CreateCertificate(path);\n        files.Add(path);\n\n        var log = new Mock<ILogger>();\n        logger.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(log.Object);\n\n        // Act\n        const bool UsePodServiceAccount = true;\n        var actual = stub.Get(UsePodServiceAccount); // !\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeOfType<KubeApiClient>();\n        actual.ApiEndPoint.ShouldNotBeNull();\n        actual.ApiEndPoint.Host.ShouldBe(expectedHost);\n        actual.ApiEndPoint.Port.ShouldBe(expectedPort);\n        actual.DefaultNamespace.ShouldNotBeNull(nameof(Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount));\n        actual.LoggerFactory.ShouldNotBeNull();\n        actual.LoggerFactory.ShouldBe(logger.Object);\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", null);\n    }\n}\n\npublic class KubeApiClientFactoryTestsBase : FileUnitTest\n{\n    protected readonly Mock<ILoggerFactory> logger = new();\n    protected readonly Mock<IOptions<KubeClientOptions>> options = new();\n    protected KubeApiClientFactory sut;\n\n    public KubeApiClientFactoryTestsBase()\n    {\n        sut = new(logger.Object, options.Object);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs",
    "content": "﻿using KubeClient.Models;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"1967\")]\npublic sealed class KubeServiceBuilderTests\n{\n    private readonly Mock<IOcelotLoggerFactory> factory;\n    private readonly Mock<IKubeServiceCreator> serviceCreator;\n    private readonly Mock<IOcelotLogger> logger;\n    private KubeServiceBuilder sut;\n\n    public KubeServiceBuilderTests()\n    {\n        factory = new();\n        serviceCreator = new();\n        logger = new();\n    }\n\n    private void Arrange()\n    {\n        factory.Setup(x => x.CreateLogger<KubeServiceBuilder>())\n            .Returns(logger.Object)\n            .Verifiable();\n        logger.Setup(x => x.LogDebug(It.IsAny<Func<string>>()))\n            .Verifiable();\n        sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object);\n    }\n\n    [Theory]\n    [InlineData(false, false)]\n    [InlineData(false, true)]\n    [InlineData(true, false)]\n    public void Cstor_NullArgs_ThrownException(bool isFactory, bool isServiceCreator)\n    {\n        // Arrange\n        var arg1 = isFactory ? factory.Object : null;\n        var arg2 = isServiceCreator ? serviceCreator.Object : null;\n\n        // Act, Assert\n        Assert.Throws<ArgumentNullException>(\n            arg1 is null ? \"factory\" : arg2 is null ? \"serviceCreator\" : string.Empty,\n            () => sut = new KubeServiceBuilder(arg1, arg2));\n    }\n\n    [Fact]\n    public void Cstor_NotNullArgs_ObjCreated()\n    {\n        // Arrange\n        factory.Setup(x => x.CreateLogger<KubeServiceBuilder>()).Verifiable();\n\n        // Act\n        sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object);\n\n        // Assert\n        Assert.NotNull(sut);\n        factory.Verify(x => x.CreateLogger<KubeServiceBuilder>(), Times.Once());\n    }\n\n    [Theory]\n    [InlineData(false, false)]\n    [InlineData(false, true)]\n    [InlineData(true, false)]\n    public void BuildServices_NullArgs_ThrownException(bool isConfiguration, bool isEndpoint)\n    {\n        // Arrange\n        var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null;\n        var arg2 = isEndpoint ? new EndpointsV1() : null;\n        Arrange();\n\n        // Act, Assert\n        Assert.Throws<ArgumentNullException>(\n            arg1 is null ? \"configuration\" : arg2 is null ? \"endpoint\" : string.Empty,\n            () => _ = sut.BuildServices(arg1, arg2));\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(1)]\n    [InlineData(2)]\n    public void BuildServices_WithSubsets_SelectedManyServicesPerSubset(int subsetCount)\n    {\n        // Arrange\n        var configuration = new KubeRegistryConfiguration();\n        var endpoint = new EndpointsV1();\n        for (int i = 1; i <= subsetCount; i++)\n        {\n            var subset = new EndpointSubsetV1();\n            subset.Addresses.Add(new() { NodeName = \"subset\" + i, Hostname = i.ToString() });\n            endpoint.Subsets.Add(subset);\n        }\n\n        serviceCreator.Setup(x => x.Create(configuration, endpoint, It.IsAny<EndpointSubsetV1>()))\n            .Returns<KubeRegistryConfiguration, EndpointsV1, EndpointSubsetV1>((c, e, s) =>\n            {\n                var item = s.Addresses[0];\n                int count = int.Parse(item.Hostname);\n                var list = new List<Service>(count);\n                while (count > 0)\n                {\n                    var id = count--.ToString();\n                    list.Add(new Service($\"{item.NodeName}-service{id}\", null, id, id, null));\n                }\n\n                return list;\n            });\n        var many = endpoint.Subsets.Sum(s => int.Parse(s.Addresses[0].Hostname));\n        Arrange();\n\n        // Act\n        var actual = sut.BuildServices(configuration, endpoint);\n\n        // Assert\n        Assert.NotNull(actual);\n        var l = actual.ToList();\n        Assert.Equal(many, l.Count);\n        serviceCreator.Verify(x => x.Create(configuration, endpoint, It.IsAny<EndpointSubsetV1>()),\n            Times.Exactly(endpoint.Subsets.Count));\n        logger.Verify(x => x.LogDebug(It.IsAny<Func<string>>()),\n            Times.Once());\n    }\n\n    [Theory]\n    [InlineData(false, false, false, false, \"K8s '?:?:?' endpoint: Total built 0 services.\")]\n    [InlineData(false, false, false, true, \"K8s '?:?:?' endpoint: Total built 0 services.\")]\n    [InlineData(false, false, true, false, \"K8s '?:?:?' endpoint: Total built 0 services.\")]\n    [InlineData(false, false, true, true, \"K8s '?:?:Name' endpoint: Total built 0 services.\")]\n    [InlineData(false, true, true, true, \"K8s '?:ApiVersion:Name' endpoint: Total built 0 services.\")]\n    [InlineData(true, true, true, true, \"K8s 'Kind:ApiVersion:Name' endpoint: Total built 0 services.\")]\n    public void BuildServices_WithEndpoint_LogDebug(bool hasKind, bool hasApiVersion, bool hasMetadata, bool hasMetadataName, string message)\n    {\n        // Arrange\n        var configuration = new KubeRegistryConfiguration();\n        var endpoint = new EndpointsV1()\n        {\n            Kind = hasKind ? nameof(EndpointsV1.Kind) : null,\n            ApiVersion = hasApiVersion ? nameof(EndpointsV1.ApiVersion) : null,\n            Metadata = hasMetadata ? new()\n            {\n                Name = hasMetadataName ? nameof(ObjectMetaV1.Name) : null,\n            } : null,\n        };\n        Arrange();\n        string actualMesssage = null;\n        logger.Setup(x => x.LogDebug(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => actualMesssage = f.Invoke());\n\n        // Act\n        var actual = sut.BuildServices(configuration, endpoint);\n\n        // Assert\n        Assert.NotNull(actual);\n        logger.Verify(x => x.LogDebug(It.IsAny<Func<string>>()),\n            Times.Once());\n        Assert.NotNull(actualMesssage);\n        Assert.Equal(message, actualMesssage);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs",
    "content": "﻿using KubeClient.Models;\nusing Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"1967\")]\npublic sealed class KubeServiceCreatorTests\n{\n    private readonly Mock<IOcelotLoggerFactory> factory;\n    private readonly Mock<IOcelotLogger> logger;\n    private KubeServiceCreator sut;\n\n    public KubeServiceCreatorTests()\n    {\n        factory = new();\n        logger = new();\n    }\n\n    private void Arrange()\n    {\n        factory.Setup(x => x.CreateLogger<KubeServiceCreator>())\n            .Returns(logger.Object)\n            .Verifiable();\n        logger.Setup(x => x.LogDebug(It.IsAny<Func<string>>()))\n            .Verifiable();\n        sut = new KubeServiceCreator(factory.Object);\n    }\n\n    [Fact]\n    public void Cstor_NullArg_ThrownException()\n    {\n        // Arrange, Act, Assert\n        Assert.Throws<ArgumentNullException>(\"factory\",\n            () => sut = new KubeServiceCreator(null));\n    }\n\n    [Fact]\n    public void Cstor_NotNullArg_ObjCreated()\n    {\n        // Arrange\n        factory.Setup(x => x.CreateLogger<KubeServiceCreator>()).Verifiable();\n\n        // Act\n        sut = new KubeServiceCreator(factory.Object);\n\n        // Assert\n        Assert.NotNull(sut);\n        factory.Verify(x => x.CreateLogger<KubeServiceCreator>(), Times.Once());\n    }\n\n    [Theory]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public void Create_NullArgs_ReturnedEmpty(bool isConfiguration, bool isEndpoint, bool isSubset)\n    {\n        // Arrange\n        var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null;\n        var arg2 = isEndpoint ? new EndpointsV1() : null;\n        var arg3 = isSubset ? new EndpointSubsetV1() : null;\n        Arrange();\n\n        // Act\n        var actual = sut.Create(arg1, arg2, arg3);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Empty(actual);\n    }\n\n    [Trait(\"Bug\", \"977\")]\n    [Fact(DisplayName = \"Create: With empty args -> No exceptions during creation\")]\n    public void Create_NotNullButEmptyArgs_CreatedEmptyService()\n    {\n        // Arrange\n        var arg1 = new KubeRegistryConfiguration()\n        {\n             KubeNamespace = nameof(KubeServiceCreatorTests),\n             KeyOfServiceInK8s = nameof(Create_NotNullButEmptyArgs_CreatedEmptyService),\n        };\n        var arg2 = new EndpointsV1();\n        var arg3 = new EndpointSubsetV1();\n        arg3.Addresses.Add(new());\n        arg2.Subsets.Add(arg3);\n        Arrange();\n\n        // Act\n        var actual = sut.Create(arg1, arg2, arg3);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.NotEmpty(actual);\n        var actualService = actual.SingleOrDefault();\n        Assert.NotNull(actualService);\n        Assert.Null(actualService.Name);\n        Assert.NotNull(actualService.HostAndPort);\n        Assert.Equal(80, actualService.HostAndPort.DownstreamPort);\n    }\n\n    [Fact]\n    public void Create_ValidArgs_HappyPath()\n    {\n        // Arrange\n        var arg1 = new KubeRegistryConfiguration()\n        {\n            KubeNamespace = nameof(KubeServiceCreatorTests),\n            KeyOfServiceInK8s = nameof(Create_ValidArgs_HappyPath),\n            Scheme = \"happy\", //nameof(HttpScheme.Http),\n        };\n        var arg2 = new EndpointsV1()\n        {\n            ApiVersion = \"v1\",\n            Metadata = new()\n            {\n                Namespace = nameof(KubeServiceCreatorTests),\n                Name = nameof(Create_ValidArgs_HappyPath),\n                Uid = Guid.NewGuid().ToString(),\n            },\n        };\n        var arg3 = new EndpointSubsetV1();\n        arg3.Addresses.Add(new()\n        {\n            Ip = \"8.8.8.8\",\n            NodeName = \"google\",\n            Hostname = \"dns.google\",\n        });\n        var ports = new List<EndpointPortV1>\n        {\n            new() { Name = nameof(HttpScheme.Http), Port = 80 },\n            new() { Name = \"happy\", Port = 888 },\n        };\n        arg3.Ports.AddRange(ports);\n        arg2.Subsets.Add(arg3);\n        Arrange();\n\n        // Act\n        var actual = sut.Create(arg1, arg2, arg3);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.NotEmpty(actual);\n        var service = actual.SingleOrDefault();\n        Assert.NotNull(service);\n        Assert.Equal(nameof(Create_ValidArgs_HappyPath), service.Name);\n        Assert.Equal(\"happy\", service.HostAndPort.Scheme);\n        Assert.Equal(888, service.HostAndPort.DownstreamPort);\n        Assert.Equal(\"8.8.8.8\", service.HostAndPort.DownstreamHost);\n        logger.Verify(x => x.LogDebug(It.IsAny<Func<string>>()),\n            Times.Once());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/KubernetesProviderFactoryTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\npublic sealed class KubernetesProviderFactoryTests : KubernetesProviderFactoryTestsBase\n{\n    [Theory]\n    [Trait(\"Bug\", \"977\")]\n    [InlineData(typeof(Kube))]\n    [InlineData(typeof(PollKube))]\n    [InlineData(typeof(WatchKube))]\n    public void CreateProvider_ClientHasOriginalLifetimeWithEnabledScopesValidation_ShouldResolveProvider(Type providerType)\n    {\n        // Arrange\n        _builder.AddKubernetes();\n        var endpointClient = new Mock<IEndPointClient>();\n        endpointClient.Setup(x => x.Watch(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns(Mock.Of<IObservable<IResourceEventV1<EndpointsV1>>>());\n        \n        var kubeClient = new Mock<IKubeApiClient>();\n        kubeClient.Setup(x => x.ResourceClient(It.IsAny<Func<IKubeApiClient, IEndPointClient>>()))\n            .Returns(endpointClient.Object);\n        var descriptor = _builder.Services.First(x => x.ServiceType == typeof(IKubeApiClient));\n        _builder.Services.Replace(ServiceDescriptor.Describe(descriptor.ServiceType, _ => kubeClient.Object, descriptor.Lifetime));\n\n        // Act\n        var actual = CreateProvider(providerType.Name);\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeOfType(providerType);\n    }\n    \n    [Theory]\n    [Trait(\"Bug\", \"977\")]\n    [InlineData(nameof(Kube))]\n    [InlineData(nameof(PollKube))]\n    [InlineData(nameof(WatchKube))]\n    public void CreateProvider_ClientHasScopedLifetimeWithEnabledScopesValidation_ShouldFailToResolve(string providerType)\n    {\n        // Arrange\n        _builder.AddKubernetes();\n        var descriptor = ServiceDescriptor.Describe(typeof(IKubeApiClient), _ => Mock.Of<IKubeApiClient>(), ServiceLifetime.Scoped);\n        _builder.Services.Replace(descriptor);\n\n        // Act\n        IServiceDiscoveryProvider actual = null;\n        var func = () => actual = CreateProvider(providerType);\n\n        // Assert\n        var ex = func.ShouldThrow<InvalidOperationException>();\n        ex.Message.ShouldContain(\"Cannot resolve scoped service 'KubeClient.IKubeApiClient' from root provider\");\n        actual.ShouldBeNull();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2256\")]\n    public void CreateProvider_KubeApiClientFactory_ShouldCreateFromOptions()\n    {\n        // Arrange\n        _builder.AddKubernetes(false); // !!!\n\n        // In app user must setup by the following:\n        //MyOptions options = new();\n        //_builder.Configuration.GetSection(nameof(MyOptions)).Bind(options);\n        var options = new Mock<IOptions<KubeClientOptions>>();\n        options.SetupGet(x => x.Value).Returns(new KubeClientOptions\n        {\n            ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, IPAddress.Loopback.ToString(), PortFinder.GetRandomPort()).Uri,\n            ClientCertificate = FakeKubeApiClientFactory.CreateCertificate(),\n            KubeNamespace = nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromOptions),\n        });\n        _builder.Services.AddSingleton<IOptions<KubeClientOptions>>(options.Object);\n\n        // Act\n        var actual = CreateProvider(nameof(Kube));\n\n        // Assert\n        actual.ShouldNotBeNull().ShouldBeOfType<Kube>();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2256\")]\n    public void CreateProvider_HasConfigureOptions_ShouldCallConfigure()\n    {\n        // Arrange\n        _builder.AddKubernetes(configureOptions: null, username: \"myUser\"); // !!!\n\n        // Act, Assert\n        var actual = CreateProvider(nameof(Kube));\n        actual.ShouldNotBeNull().ShouldBeOfType<Kube>();\n\n        // Act, Assert\n        var provider = _builder.Services.BuildServiceProvider(true);\n        var o = provider.GetService<IOptions<KubeClientOptions>>().ShouldNotBeNull();\n        o.Value.ShouldNotBeNull().Username.ShouldBe(\"myUser\");\n\n        // Act, Assert\n        var configureOptions = provider.GetService<IConfigureOptions<KubeClientOptions>>().ShouldNotBeNull();\n        var opts = new KubeClientOptions();\n        configureOptions.Configure(opts);\n        opts.Username.ShouldBe(\"myUser\");\n    }\n}\n\n[Collection(nameof(SequentialTests))]\npublic sealed class KubernetesProviderFactorySequentialTests : KubernetesProviderFactoryTestsBase\n{\n    [Fact]\n    [Trait(\"Feat\", \"2256\")]\n    public async Task CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount()\n    {\n        // Arrange\n        _builder.AddKubernetes(true); // !!!\n        var serviceAccountPath = Path.Combine(AppContext.BaseDirectory, TestID);\n        var stub = new FakeKubeApiClientFactory(null, null, serviceAccountPath);\n        var original = _builder.Services.First(x => x.ServiceType == typeof(IKubeApiClientFactory));\n        var descriptor = ServiceDescriptor.Describe(original.ServiceType, _ => stub, original.Lifetime);\n        _builder.Services.Replace(descriptor);\n\n        var expectedHost = IPAddress.Loopback.ToString();\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_HOST\", expectedHost);\n        int expectedPort = PortFinder.GetRandomPort();\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", expectedPort.ToString());\n\n        folders.Add(serviceAccountPath);\n        if (!Directory.Exists(serviceAccountPath))\n        {\n            Directory.CreateDirectory(serviceAccountPath);\n        }\n\n        var path = Path.Combine(serviceAccountPath, \"namespace\");\n        await File.WriteAllTextAsync(path, nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount), TestContext.Current.CancellationToken);\n        files.Add(path);\n\n        path = Path.Combine(serviceAccountPath, \"token\");\n        await File.WriteAllTextAsync(path, TestID, TestContext.Current.CancellationToken);\n        files.Add(path);\n\n        path = Path.Combine(serviceAccountPath, \"ca.crt\");\n        await FakeKubeApiClientFactory.CreateCertificate(path);\n        files.Add(path);\n\n        // Act\n        var actualProvider = CreateProvider(nameof(Kube));\n\n        // Assert\n        actualProvider.ShouldNotBeNull().ShouldBeOfType<Kube>();\n        stub.ShouldNotBeNull();\n        stub.Actual.ShouldNotBeNull();\n        stub.Actual.ApiEndPoint.ShouldNotBeNull();\n        stub.Actual.ApiEndPoint.Host.ShouldBe(expectedHost);\n        stub.Actual.ApiEndPoint.Port.ShouldBe(expectedPort);\n        stub.Actual.DefaultNamespace.ShouldNotBeNull(nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount));\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", null);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2299\")]\n    public void Bug2299_StepsToReproduce_ShouldNotThrowExceptionByPathCombine()\n    {\n        // Arrange\n        _builder.AddKubernetes(); // !!!\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_HOST\", \"localhost\");\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", PortFinder.GetRandomPort().ToString());\n\n        // Act\n        var ex = Assert.ThrowsAny<Exception>(\n            () => CreateProvider(nameof(Kube)));\n\n        // Assert\n        ex.ShouldNotBeOfType<ArgumentNullException>();\n        ex.ShouldBeOfType<DirectoryNotFoundException>();\n        ex.StackTrace.ShouldContain(\"at KubeClient.KubeClientOptions.FromPodServiceAccount(String serviceAccountPath)\");\n        ex.StackTrace.ShouldNotContain(\"at System.IO.Path.Combine(String path1, String path2)\");\n        ex.Message.ShouldNotBe(\"Value cannot be null. (Parameter 'path1')\");\n        Environment.SetEnvironmentVariable(\"KUBERNETES_SERVICE_PORT\", null);\n    }\n}\n\npublic class KubernetesProviderFactoryTestsBase : FileUnitTest\n{\n    protected readonly IOcelotBuilder _builder;\n\n    public KubernetesProviderFactoryTestsBase()\n    {\n        var config = new FileConfiguration();\n        config.GlobalConfiguration.ServiceDiscoveryProvider = new()\n        {\n            Scheme = Uri.UriSchemeHttp,\n            Host = \"localhost\",\n            Port = 888,\n            Namespace = nameof(KubernetesProviderFactoryTests),\n            Token = TestID,\n        };\n        var configuration = new ConfigurationBuilder()\n            .SetBasePath(AppContext.BaseDirectory)\n            .AddOcelot(config, null, MergeOcelotJson.ToMemory)\n            .Build();\n        _builder = new ServiceCollection().AddOcelot(configuration);\n    }\n\n    protected IServiceDiscoveryProvider CreateProvider(string providerType)\n    {\n        var serviceProvider = _builder.Services.BuildServiceProvider(true);\n        var config = GivenServiceProvider(providerType);\n        var route = GivenRoute();\n        return serviceProvider\n            .GetRequiredService<ServiceDiscoveryFinderDelegate>() // returns KubernetesProviderFactory.Get instance\n            .Invoke(serviceProvider, config, route);\n    }\n\n    protected static ServiceProviderConfiguration GivenServiceProvider(string type) => new()\n    {\n        Type = type,\n        Scheme = string.Empty,\n        Host = string.Empty,\n        Port = 1,\n        Token = string.Empty,\n        ConfigurationKey = string.Empty,\n        PollingInterval = 9_000,\n    };\n\n    private static DownstreamRoute GivenRoute([CallerMemberName] string serviceName = nameof(KubernetesProviderFactoryTests))\n        => new DownstreamRouteBuilder().WithServiceName(serviceName).Build();\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/ObservableExtensionsTests.cs",
    "content": "﻿using Microsoft.Reactive.Testing;\nusing Ocelot.Provider.Kubernetes;\nusing System.Reactive.Disposables;\nusing System.Reactive.Linq;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"2168\")]\n[Trait(\"PR\", \"2174\")] // https://github.com/ThreeMammals/Ocelot/pull/2174\npublic class ObservableExtensionsTests\n{\n    private readonly TestScheduler _testScheduler = new();\n    \n    [Fact]\n    public async Task RetryAfter_ExceptionThrown_RetriesInfiniteWithDelay()\n    {\n        // Arrange\n        var errorsToThrow = Random.Shared.Next(10, 1000);\n        var errorsCounter = 0;\n        var expectedResult = 123;\n        var delaySeconds = TimeSpan.FromSeconds(3);\n        var observable = Observable.Create<int>(observer =>\n        {\n            if (errorsCounter < errorsToThrow)\n            {\n                errorsCounter++;\n                throw new Exception(\"Need to catch and retry\");\n            }\n\n            observer.OnNext(expectedResult);\n            return Disposable.Empty;\n        });\n        \n        // Act\n        using var cts = new CancellationTokenSource();\n        _ = Task.Run(() =>\n        {\n            // have to spin in separate thread because it is used after first subscription and stops after first Exception\n            while (!cts.Token.IsCancellationRequested)\n            {\n                _testScheduler.Start();\n            }\n        }, TestContext.Current.CancellationToken);\n        \n        var result = await observable.RetryAfter(delaySeconds, _testScheduler).FirstAsync();\n        await cts.CancelAsync();\n        \n        // Assert\n        result.ShouldBe(expectedResult);\n        errorsCounter.ShouldBe(errorsToThrow);\n        _testScheduler.Clock.ShouldBe(delaySeconds.Ticks * errorsToThrow);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs",
    "content": "﻿using KubeClient;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.ServiceDiscovery;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\npublic class OcelotBuilderExtensionsTests : UnitTest // No Chinese tests now!\n{\n    private readonly IServiceCollection _services;\n    private readonly IConfiguration _configRoot;\n    private IOcelotBuilder _ocelotBuilder;\n\n    public OcelotBuilderExtensionsTests()\n    {\n        _configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());\n        _services = new ServiceCollection();\n        _services.AddSingleton(GetHostingEnvironment());\n        _services.AddSingleton(_configRoot);\n    }\n\n    private static IWebHostEnvironment GetHostingEnvironment()\n    {\n        var environment = new Mock<IWebHostEnvironment>();\n        environment.Setup(e => e.ApplicationName)\n            .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name);\n        return environment.Object;\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"345\")]\n    public void AddKubernetes_NoExceptions_ShouldSetUpKubernetes()\n    {\n        // Arrange\n        var addOcelot = () => _ocelotBuilder = _services.AddOcelot(_configRoot);\n        addOcelot.ShouldNotThrow();\n\n        // Act\n        var addKubernetes = () => _ocelotBuilder.AddKubernetes();\n\n        // Assert\n        addKubernetes.ShouldNotThrow();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"977\")]\n    [Trait(\"PR\", \"2180\")]\n    public void AddKubernetes_DefaultServices_HappyPath()\n    {\n        // Arrange, Act\n        _ocelotBuilder = _services.AddOcelot(_configRoot).AddKubernetes();\n\n        // Assert\n        AssertServices();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2256\")]\n    public void AddKubernetes_NoAction_HappyPath()\n    {\n        // Arrange\n        Action<KubeClientOptions> noAction = null;\n        _ocelotBuilder = _services.AddOcelot(_configRoot);\n\n        // Act\n        _ocelotBuilder.AddKubernetes(noAction);\n\n        // Assert\n        AssertServices();\n        Assert<IConfigureOptions<KubeClientOptions>>(); // not IOptions<KubeClientOptions>\n    }\n\n    private void AssertServices()\n    {\n        Assert<IKubeApiClient>(); // 2180 scenario\n        Assert<ServiceDiscoveryFinderDelegate>();\n        Assert<IKubeServiceBuilder>();\n        Assert<IKubeServiceCreator>();\n    }\n\n    private void Assert<T>(ServiceLifetime lifetime = ServiceLifetime.Singleton)\n        where T : class\n    {\n        var descriptor = _services.SingleOrDefault(Of<T>).ShouldNotBeNull();\n        descriptor.Lifetime.ShouldBe(lifetime);\n    }\n\n    private static bool Of<T>(ServiceDescriptor descriptor)\n        where T : class\n        => descriptor.ServiceType.Equals(typeof(T));\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs",
    "content": "﻿using Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\nusing System.Collections.Concurrent;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"345\")] // https://github.com/ThreeMammals/Ocelot/issues/345\npublic sealed class PollKubeTests : UnitTest, IDisposable\n{\n    private PollKube _provider;\n    private readonly Mock<IOcelotLoggerFactory> _factory = new();\n    private readonly Mock<IOcelotLogger> _logger = new();\n    private readonly Mock<IServiceDiscoveryProvider> _discoveryProvider = new();\n\n    const int PollingIntervalMs = 1;\n\n    public PollKubeTests()\n    {\n        _factory.Setup(x => x.CreateLogger<PollKube>()).Returns(_logger.Object);\n    }\n\n    public void Dispose()\n    {\n        _provider?.Dispose();\n    }\n\n    [Fact]\n    public void Dispose_Manually()\n    {\n        var instance = new PollKube(10_000, _factory.Object, _discoveryProvider.Object);\n        instance.Dispose();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"772\")] // https://github.com/ThreeMammals/Ocelot/pull/772\n    public void Should_return_service_from_kube()\n    {\n        // Arrange\n        var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List<string>());\n        List<Service> services = [service];\n        _discoveryProvider.Setup(x => x.GetAsync()).ReturnsAsync(services);\n        _provider = new PollKube(PollingIntervalMs, _factory.Object, _discoveryProvider.Object);\n\n        // Act\n        var actual = WhenIGetTheServices(1);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(1, actual.Count);\n    }\n\n    private List<Service> WhenIGetTheServices(int expected)\n    {\n        List<Service> services = null;\n        var result = Wait.For(3_000).Until(() =>\n        {\n            try\n            {\n                services = _provider.GetAsync().GetAwaiter().GetResult();\n                return services.Count == expected;\n            }\n            catch (Exception)\n            {\n                return false;\n            }\n        });\n        Assert.True(result);\n        return services;\n    }\n\n    [Fact(Skip = \"Require coverage checks\")]\n    [Trait(\"Bug\", \"2304\")] // https://github.com/ThreeMammals/Ocelot/issues/2304\n    public async Task OnTimerCallbackAsync_AvoidPolling_WhenAlreadyPolling()\n    {\n        // Arrange\n        int pollingInterval = 100;\n        var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List<string>());\n        List<Service> services = [service];\n        var slowPolling = Task.Delay(pollingInterval + 50, TestContext.Current.CancellationToken)\n            .ContinueWith(x => services);\n        _discoveryProvider.Setup(x => x.GetAsync()).Returns(slowPolling);\n        _provider = new PollKube(pollingInterval, _factory.Object, _discoveryProvider.Object);\n\n        // Act\n        var coldRequestTask = _provider.GetAsync(); // calls Poll() due to empty queue\n        var method = _provider.GetType().GetMethod(\"OnTimerCallbackAsync\", BindingFlags.Instance | BindingFlags.NonPublic);\n        method.Invoke(_provider, [new object()]);\n        _discoveryProvider.Verify(x => x.GetAsync(), Times.Once);\n\n        var actual = await coldRequestTask;\n        _discoveryProvider.Verify(x => x.GetAsync(), Times.AtLeastOnce); // ideally it shoud be called once, but it is called 1 or 2 times. TODO A disposing enhancement?\n\n        method.Invoke(_provider, [new object()]);\n        _discoveryProvider.Verify(x => x.GetAsync(), Times.AtLeast(2));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2304\")] // https://github.com/ThreeMammals/Ocelot/issues/2304\n    public async Task GetAsync()\n    {\n        // Arrange\n        int pollingInterval = 100;\n        var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List<string>());\n        List<Service> services = [service];\n        var slowPolling = Task.Delay(pollingInterval + 50, TestContext.Current.CancellationToken).ContinueWith(x => services);\n        _discoveryProvider.Setup(x => x.GetAsync()).Returns(slowPolling);\n        _provider = new PollKube(pollingInterval, _factory.Object, _discoveryProvider.Object);\n\n        FieldInfo pollingField = _provider.GetType().GetField(\"_polling\", BindingFlags.Instance | BindingFlags.NonPublic);\n        pollingField.SetValue(_provider, true);\n        FieldInfo queueField = _provider.GetType().GetField(\"_queue\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var queue = queueField.GetValue(_provider) as ConcurrentQueue<List<Service>>;\n        List<Service> oldVersion = [service];\n        queue.Enqueue(oldVersion);\n\n        // Act\n        var actual = await _provider.GetAsync(); // will NOT call Poll()\n        Assert.Same(oldVersion, actual);\n        _discoveryProvider.Verify(x => x.GetAsync(), Times.Never);\n\n        // Scenario 2: For services with multiple versions, remove outdated versions and retain only the latest one\n        pollingField.SetValue(_provider, false);\n        List<Service> latestVersion = [new Service(\"\", new(\"h\", 123), \"\", \"\", default)];\n        queue.Enqueue(latestVersion);\n        Assert.Equal(2, queue.Count);\n\n        actual = await _provider.GetAsync(); // will NOT call Poll()\n        Assert.Equal(1, queue.Count);\n        Assert.Same(latestVersion, actual);\n        _discoveryProvider.Verify(x => x.GetAsync(), Times.Never);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Kubernetes/WatchKubeTests.cs",
    "content": "﻿using KubeClient;\nusing KubeClient.Models;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Reactive.Testing;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Provider.Kubernetes.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Linq.Expressions;\nusing System.Reactive.Linq;\n\nnamespace Ocelot.UnitTests.Kubernetes;\n\n[Trait(\"Feat\", \"2168\")]\n[Trait(\"PR\", \"2174\")] // https://github.com/ThreeMammals/Ocelot/pull/2174\npublic class WatchKubeTests\n{\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory = new();\n    private readonly Mock<IKubeApiClient> _kubeApiClient = new();\n    private readonly Mock<IEndPointClient> _endpointClient = new();\n    private readonly Mock<IKubeServiceBuilder> _kubeServiceBuilder = new();\n    private readonly TestScheduler _testScheduler = new();\n    private readonly KubeRegistryConfiguration _config = new()\n    {\n        KubeNamespace = \"dummy-namespace\", KeyOfServiceInK8s = \"dummy-service\",\n    };\n    private readonly OcelotLogger _ocLogger;\n    private readonly Mock<ILogger> _logger = new();\n    private readonly Mock<IRequestScopedDataRepository> _dataRepository = new();\n    private readonly Expression<Func<IEndPointClient, IObservable<IResourceEventV1<EndpointsV1>>>> _watch;\n\n    public WatchKubeTests()\n    {\n        _logger.Setup(x => x.IsEnabled(It.IsAny<LogLevel>()))\n            .Returns(true);\n        _logger.Setup(x => x.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()))\n            .Verifiable();\n        _dataRepository.Setup(x => x.Get<string>(It.IsAny<string>()))\n            .Returns(new OkResponse<string>(\"123\"));\n        _ocLogger = new(_logger.Object, _dataRepository.Object);\n        _loggerFactory.Setup(x => x.CreateLogger<WatchKube>())\n            .Returns(_ocLogger);\n        _kubeApiClient.Setup(x => x.ResourceClient(It.IsAny<Func<IKubeApiClient, IEndPointClient>>()))\n            .Returns(_endpointClient.Object);\n        _kubeServiceBuilder.Setup(x => x.BuildServices(It.IsAny<KubeRegistryConfiguration>(), It.IsAny<EndpointsV1>()))\n            .Returns((KubeRegistryConfiguration config, EndpointsV1 endpoints) =>\n            {\n                return endpoints.Subsets.Select((x, i) => new Service(\n                    config.KeyOfServiceInK8s,\n                    new ServiceHostAndPort(x.Addresses[i].Hostname, x.Ports[i].Port!.Value),\n                    i.ToString(),\n                    endpoints.ApiVersion,\n                    Enumerable.Empty<string>()));\n            });\n        _watch = x => x.Watch(It.Is<string>(s => s == _config.KeyOfServiceInK8s), It.IsAny<string>(), It.IsAny<CancellationToken>());\n    }\n\n    [Theory]\n    [InlineData(ResourceEventType.Added, 1)]\n    [InlineData(ResourceEventType.Modified, 1)]\n    [InlineData(ResourceEventType.Bookmark, 1)]\n    [InlineData(ResourceEventType.Error, 0)]\n    [InlineData(ResourceEventType.Deleted, 0)]\n    public async Task GetAsync_EndpointsEventObserved_ServicesReturned(ResourceEventType eventType, int expectedServicesCount)\n    {\n        // Arrange\n        var eventDelay = TimeSpan.FromMilliseconds(Random.Shared.Next(1, (WatchKube.FirstResultsFetchingTimeoutSeconds * 1000) - 1));\n        var endpointsObservable = CreateOneEvent(eventType).ToObservable().Delay(eventDelay, _testScheduler);\n        _endpointClient.Setup(_watch).Returns(endpointsObservable);\n\n        // Act\n        var watchKube = CreateWatchKube();\n        _testScheduler.AdvanceBy(eventDelay.Ticks);\n        var services = await watchKube.GetAsync();\n\n        // Assert\n        services.Count.ShouldBe(expectedServicesCount);\n    }\n\n    [Fact]\n    public async Task GetAsync_NoEventsAfterTimeout_EmptyServicesReturned()\n    {\n        // Arrange\n        _endpointClient.Setup(_watch)\n            .Returns(Observable.Create<IResourceEventV1<EndpointsV1>>(_ => Mock.Of<IDisposable>()));\n\n        // Act\n        var watchKube = CreateWatchKube();\n        _testScheduler.Start();\n        var services = await watchKube.GetAsync();\n\n        // Assert\n        services.ShouldBeEmpty();\n        _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FirstResultsFetchingTimeoutSeconds).Ticks);\n        _logger.Verify(\n            x => x.Log(LogLevel.Warning, It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public async Task GetAsync_WatchFailed_RetriedAfterDelay()\n    {\n        // Arrange\n        var subscriptionAttempts = 0;\n        var observable = Observable.Create<IResourceEventV1<EndpointsV1>>(observer =>\n        {\n            if (subscriptionAttempts == 0)\n            {\n                observer.OnError(new HttpRequestException(\"Error occured in first watch request\"));\n            }\n            else\n            {\n                observer.OnNext(CreateOneEvent(ResourceEventType.Added).First());\n            }\n\n            subscriptionAttempts++;\n            return Mock.Of<IDisposable>();\n        });\n        _endpointClient.Setup(_watch).Returns(observable);\n\n        // Act\n        var watchKube = CreateWatchKube();\n        _testScheduler.Start();\n        var services = await watchKube.GetAsync();\n\n        // Assert\n        services.Count.ShouldBe(1);\n        subscriptionAttempts.ShouldBe(2);\n        _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FailedSubscriptionRetrySeconds).Ticks);\n        _logger.Verify(\n            x => x.Log(LogLevel.Error, It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public async Task Dispose_OnSubscriptionCancellation_LogsInformation()\n    {\n        // Arrange\n        var observable = Observable.Create<IResourceEventV1<EndpointsV1>>(observer =>\n        {\n            observer.OnCompleted();\n            return Mock.Of<IDisposable>();\n        });\n        _endpointClient.Setup(_watch).Returns(observable);\n\n        // Act\n        var watchKube = CreateWatchKube();\n        _testScheduler.Start();\n        var services = await watchKube.GetAsync();\n        watchKube.Dispose();\n\n        // Assert\n        services.ShouldBeEmpty();\n        _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FirstResultsFetchingTimeoutSeconds).Ticks);\n        _logger.Verify(\n            x => x.Log(LogLevel.Information, It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()),\n            Times.Once());\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task GetAsync_EndpointsEventObserved_NoServices(bool branch1)\n    {\n        // Arrange\n        var eventDelay = TimeSpan.FromMilliseconds(Random.Shared.Next(1, (WatchKube.FirstResultsFetchingTimeoutSeconds * 1000) - 1));\n        EndpointsV1 endpoints = null;\n        if (branch1)\n        {\n            endpoints = new EndpointsV1\n            {\n                Kind = \"endpoint\", ApiVersion = \"1.0\",\n                Metadata = new ObjectMetaV1 { Name = _config.KeyOfServiceInK8s, Namespace = _config.KubeNamespace, },\n            };\n            endpoints.Subsets.Clear();\n        }\n\n        var resourceEvent = new ResourceEventV1<EndpointsV1> { EventType = ResourceEventType.Bookmark, Resource = endpoints, };\n        var events = new ResourceEventV1<EndpointsV1>[] { resourceEvent };\n        var endpointsObservable = events.ToObservable().Delay(eventDelay, _testScheduler);\n        _endpointClient.Setup(_watch).Returns(endpointsObservable);\n\n        // Act\n        var watchKube = CreateWatchKube();\n        _testScheduler.AdvanceBy(eventDelay.Ticks);\n        var services = await watchKube.GetAsync();\n\n        // Assert\n        services.ShouldBeEmpty();\n    }\n\n    [Theory]\n    [InlineData(-1, 1)]\n    [InlineData(0, 1)]\n    [InlineData(1, 1)]\n    [InlineData(3, 3)]\n    public void StaticProperties_Setter_ShouldBeGreaterThanOrEqualToOne(int value, int expected)\n    {\n        WatchKube.FailedSubscriptionRetrySeconds = value;\n        Assert.Equal(expected, WatchKube.FailedSubscriptionRetrySeconds);\n\n        WatchKube.FirstResultsFetchingTimeoutSeconds = value;\n        Assert.Equal(expected, WatchKube.FirstResultsFetchingTimeoutSeconds);\n    }\n\n    private WatchKube CreateWatchKube() => new(_config, _loggerFactory.Object, _kubeApiClient.Object, _kubeServiceBuilder.Object, _testScheduler);\n\n    private IResourceEventV1<EndpointsV1>[] CreateOneEvent(ResourceEventType eventType)\n    {\n        var resourceEvent = new ResourceEventV1<EndpointsV1> { EventType = eventType, Resource = CreateEndpoints(), };\n        return [resourceEvent];\n    }\n\n    private EndpointsV1 CreateEndpoints()\n    {\n        var endpoints = new EndpointsV1\n        {\n            Kind = \"endpoint\",\n            ApiVersion = \"1.0\",\n            Metadata = new ObjectMetaV1 { Name = _config.KeyOfServiceInK8s, Namespace = _config.KubeNamespace, },\n        };\n        var subset = new EndpointSubsetV1();\n        subset.Addresses.Add(new EndpointAddressV1 { Ip = \"127.0.0.1\", Hostname = \"localhost\" });\n        subset.Ports.Add(new EndpointPortV1 { Port = 80 });\n        endpoints.Subsets.Add(subset);\n        return endpoints;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.ServiceDiscovery.Providers;\n\r\nnamespace Ocelot.UnitTests.LoadBalancer;\r\n\r\npublic class CookieStickySessionsCreatorTests : UnitTest\r\n{\r\n    private readonly CookieStickySessionsCreator _creator;\r\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\r\n\r\n    public CookieStickySessionsCreatorTests()\r\n    {\r\n        _creator = new();\r\n        _serviceProvider = new();\r\n    }\n\n    [Fact]\r\n    public void Should_return_instance_of_expected_load_balancer_type()\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithLoadBalancerOptions(new LoadBalancerOptions(\"myType\", \"myKey\", 1000))\r\n            .Build();\r\n\r\n        // Act\r\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\r\n\r\n        // Assert\r\n        loadBalancer.Data.ShouldBeOfType<CookieStickySessions>();\r\n    }\n\n    [Fact]\r\n    public void Should_return_expected_name()\r\n    {\r\n        // Arrange, Act, Assert\r\n        _creator.Type.ShouldBe(nameof(CookieStickySessions));\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Infrastructure;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\nusing Ocelot.Values;\nusing System.Collections;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\n[Trait(\"Feat\", \"336\")]\npublic sealed class CookieStickySessionsTests : UnitTest\n{\n    private readonly CookieStickySessions _stickySessions;\n    private readonly Mock<ILoadBalancer> _loadBalancer;\n    private readonly int _defaultExpiryInMs;\n    private readonly FakeBus<StickySession> _bus;\n    private readonly DefaultHttpContext _httpContext;\n\n    public CookieStickySessionsTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _bus = new FakeBus<StickySession>();\n        _loadBalancer = new Mock<ILoadBalancer>();\n        _defaultExpiryInMs = 0;\n        _stickySessions = new CookieStickySessions(_loadBalancer.Object, \"sessionid\", _defaultExpiryInMs, _bus);\n    }\n\n    private void Arrange([CallerMemberName] string serviceName = null)\n    {\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(serviceName)\n            .Build();\n        _httpContext.Items.UpsertDownstreamRoute(route);\n    }\n\n    [Fact]\n    public async Task Should_return_host_and_port()\n    {\n        Arrange();\n        GivenTheLoadBalancerReturns();\n        GivenTheDownstreamRequestHasSessionId(\"321\");\n\n        // Act\n        var result = await _stickySessions.LeaseAsync(_httpContext);\n\n        // Assert\n        result.Data.ShouldNotBeNull();\n    }\n\n    [Fact]\n    public async Task Should_return_same_host_and_port()\n    {\n        Arrange();\n        GivenTheLoadBalancerReturnsSequence();\n        GivenTheDownstreamRequestHasSessionId(\"321\");\n\n        // Act\n        var firstHostAndPort = await _stickySessions.LeaseAsync(_httpContext);\n        var secondHostAndPort = await _stickySessions.LeaseAsync(_httpContext);\n\n        // Assert\n        firstHostAndPort.Data.DownstreamHost.ShouldBe(secondHostAndPort.Data.DownstreamHost);\n        firstHostAndPort.Data.DownstreamPort.ShouldBe(secondHostAndPort.Data.DownstreamPort);\n        _bus.Messages.Count.ShouldBe(2);\n    }\n\n    [Fact]\n    public async Task Should_return_different_host_and_port_if_load_balancer_does()\n    {\n        Arrange();\n        GivenTheLoadBalancerReturnsSequence();\n\n        // When I Make Two Requets With Different Session Values\n        var contextOne = new DefaultHttpContext();\n        var cookiesOne = new FakeCookies();\n        cookiesOne.AddCookie(\"sessionid\", \"321\");\n        contextOne.Request.Cookies = cookiesOne;\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(nameof(Should_return_different_host_and_port_if_load_balancer_does))\n            .Build();\n        contextOne.Items.UpsertDownstreamRoute(route);\n\n        var contextTwo = new DefaultHttpContext();\n        var cookiesTwo = new FakeCookies();\n        cookiesTwo.AddCookie(\"sessionid\", \"123\");\n        contextTwo.Request.Cookies = cookiesTwo;\n        contextTwo.Items.UpsertDownstreamRoute(route);\n\n        // Act\n        var firstHostAndPort = await _stickySessions.LeaseAsync(contextOne);\n        var secondHostAndPort = await _stickySessions.LeaseAsync(contextTwo);\n\n        // Assert\n        firstHostAndPort.Data.DownstreamHost.ShouldBe(\"one\");\n        firstHostAndPort.Data.DownstreamPort.ShouldBe(80);\n        secondHostAndPort.Data.DownstreamHost.ShouldBe(\"two\");\n        secondHostAndPort.Data.DownstreamPort.ShouldBe(80);\n    }\n\n    [Fact]\n    public async Task Should_return_error()\n    {\n        Arrange();\n        _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new ErrorResponse<ServiceHostAndPort>(new AnyError()));\n\n        // Act\n        var result = await _stickySessions.LeaseAsync(_httpContext);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public async Task Should_expire_sticky_session()\n    {\n        Arrange();\n        GivenTheLoadBalancerReturns();\n        GivenTheDownstreamRequestHasSessionId(\"321\");\n        GivenIHackAMessageInWithAPastExpiry();\n\n        // Act\n        var result = await _stickySessions.LeaseAsync(_httpContext);\n        _bus.Process();\n\n        // Assert\n        _loadBalancer.Verify(x => x.Release(It.IsAny<ServiceHostAndPort>()), Times.Once);\n    }\n\n    [Fact]\n    public void Should_release()\n    {\n        // Arrange, Act, Assert\n        _stickySessions.Release(new ServiceHostAndPort(string.Empty, 0));\n    }\n\n    [Fact]\n    public void Type_Is_CookieStickySessions()\n    {\n        // Arrange, Act, Assert\n        Assert.Equal(\"CookieStickySessions\", _stickySessions.Type);\n    }\n\n    private void GivenIHackAMessageInWithAPastExpiry()\n    {\n        var hostAndPort = new ServiceHostAndPort(\"999\", 999);\n        _bus.Publish(new StickySession(hostAndPort, DateTime.UtcNow.AddDays(-1), \"321\"), 0);\n    }\n\n    private void GivenTheLoadBalancerReturnsSequence()\n    {\n        _loadBalancer\n            .SetupSequence(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(new ServiceHostAndPort(\"one\", 80)))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(new ServiceHostAndPort(\"two\", 80)));\n    }\n\n    private void GivenTheDownstreamRequestHasSessionId(string value)\n    {\n        var cookies = new FakeCookies();\n        cookies.AddCookie(\"sessionid\", value);\n        _httpContext.Request.Cookies = cookies;\n    }\n\n    private void GivenTheLoadBalancerReturns()\n    {\n        _loadBalancer\n            .Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(new ServiceHostAndPort(string.Empty, 80)));\n    }\n}\n\ninternal class FakeCookies : IRequestCookieCollection\n{\n    private readonly Dictionary<string, string> _cookies = new();\n    public string this[string key] => _cookies[key];\n    public int Count => _cookies.Count;\n    public ICollection<string> Keys => _cookies.Keys;\n    public void AddCookie(string key, string value) => _cookies[key] = value;\n    public bool ContainsKey(string key) => _cookies.ContainsKey(key);\n    public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => _cookies.GetEnumerator();\n    public bool TryGetValue(string key, out string value) => _cookies.TryGetValue(key, out value);\n    IEnumerator IEnumerable.GetEnumerator() => _cookies.GetEnumerator();\n}\n\ninternal class FakeBus<T> : IBus<T>\n{\n    public FakeBus()\n    {\n        Messages = new List<T>();\n        Subscriptions = new List<Action<T>>();\n    }\n\n    public List<T> Messages { get; }\n    public List<Action<T>> Subscriptions { get; }\n\n    public void Subscribe(Action<T> action) => Subscriptions.Add(action);\n    public void Publish(T message, int delay) => Messages.Add(message);\n\n    public void Process()\n    {\n        foreach (var message in Messages)\n        {\n            foreach (var subscription in Subscriptions)\n            {\n                subscription(message);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class DelegateInvokingLoadBalancerCreatorTests : UnitTest\n{\n    private DelegateInvokingLoadBalancerCreator<FakeLoadBalancer> _creator;\n    private Func<DownstreamRoute, IServiceDiscoveryProvider, ILoadBalancer> _creatorFunc;\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\n\n    public DelegateInvokingLoadBalancerCreatorTests()\n    {\n        _creatorFunc = (route, serviceDiscoveryProvider) =>\n            new FakeLoadBalancer(route, serviceDiscoveryProvider);\n        _creator = new DelegateInvokingLoadBalancerCreator<FakeLoadBalancer>(_creatorFunc);\n        _serviceProvider = new Mock<IServiceDiscoveryProvider>();\n    }\n\n    [Fact]\n    public void Should_return_expected_name()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder().Build();\n\n        // Act\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\n\n        // Assert\n        loadBalancer.Data.Type.ShouldBe(nameof(FakeLoadBalancer));\n    }\n\n    [Fact]\n    public void Should_return_result_of_specified_creator_func()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder().Build();\n\n        // Act\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\n\n        // Assert\n        loadBalancer.Data.ShouldBeOfType<FakeLoadBalancer>();\n    }\n\n    [Fact]\n    public void Should_return_error()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder().Build();\n        _creatorFunc = (route, serviceDiscoveryProvider) => throw new Exception();\n        _creator = new DelegateInvokingLoadBalancerCreator<FakeLoadBalancer>(_creatorFunc);\n\n        // Act\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\n\n        // Assert\n        loadBalancer.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Type()\n    {\n        // Arrange, Act, Assert\n        Assert.Equal(nameof(FakeLoadBalancer), _creator.Type);\n    }\n\n    private class FakeLoadBalancer : ILoadBalancer\n    {\n        public FakeLoadBalancer(DownstreamRoute downstreamRoute, IServiceDiscoveryProvider serviceDiscoveryProvider)\n        {\n            DownstreamRoute = downstreamRoute;\n            ServiceDiscoveryProvider = serviceDiscoveryProvider;\n        }\n\n        public DownstreamRoute DownstreamRoute { get; }\n        public IServiceDiscoveryProvider ServiceDiscoveryProvider { get; }\n        public string Type => nameof(FakeLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LeaseEventArgsTests.cs",
    "content": "﻿using Ocelot.LoadBalancer;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LeaseEventArgsTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange\n        ServiceHostAndPort host = new(\"host\", 123);\n        Lease lease = new(host, 3);\n        Service service = new(\"s\", new(\"h\", 123), \"\", \"\", []);\n\n        // Act\n        LeaseEventArgs args = new(lease, service, 3);\n\n        // Assert\n        Assert.Equal(lease, args.Lease);\n        Assert.Equal(service, args.Service);\n        Assert.Equal(3, args.ServiceIndex);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LeaseTests.cs",
    "content": "﻿using Ocelot.LoadBalancer;\nusing Ocelot.Values;\nusing Steeltoe.Connector;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LeaseTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        Lease l = new();\n\n        // Assert\n        Assert.Null(l.HostAndPort);\n        Assert.Equal(0, l.Connections);\n    }\n\n    [Fact]\n    public void Ctor_Lease()\n    {\n        // Arrange\n        ServiceHostAndPort host = new(\"host\", 123);\n        Lease from = new(host, 3);\n\n        // Act\n        Lease actual = new(from);\n\n        // Assert\n        Assert.Equivalent(from, actual);\n        Assert.Equivalent(3, actual.Connections);\n    }\n\n    [Fact]\n    public void Ctor_ServiceHostAndPort()\n    {\n        // Arrange\n        ServiceHostAndPort hostAndPort = new(\"host\", 123);\n\n        // Act\n        Lease actual = new(hostAndPort);\n\n        // Assert\n        Assert.Equivalent(hostAndPort, actual.HostAndPort);\n        Assert.Equal(hostAndPort, actual.HostAndPort);\n        Assert.Equal(0, actual.Connections);\n    }\n\n    [Fact]\n    public void Ctor_Init()\n    {\n        // Arrange\n        ServiceHostAndPort hostAndPort = new(\"host\", 123);\n        int connections = 3;\n\n        // Act\n        Lease actual = new(hostAndPort, connections);\n\n        // Assert\n        Assert.Equivalent(hostAndPort, actual.HostAndPort);\n        Assert.Equal(hostAndPort, actual.HostAndPort);\n        Assert.Equal(3, actual.Connections);\n    }\n\n    [Fact]\n    public void Null()\n    {\n        // Arrange, Act\n        Lease actual = Lease.Null;\n\n        // Assert\n        Assert.Null(actual.HostAndPort);\n        Assert.Equal(0, actual.Connections);\n    }\n\n    [Fact]\n    public void ToString_HostPlusConnections()\n    {\n        // Arrange\n        Lease l = new(new(\"host\", 333, \"ws\"), 4);\n\n        // Act\n        var actual = l.ToString();\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(\"(ws:host:333+4)\", actual);\n    }\n\n    [Fact]\n    public void Equals_object()\n    {\n        // Arrange, Act, Assert\n        Lease l = Lease.Null;\n        var boxed = (object)l;\n        bool equality = l.Equals(boxed);\n        Assert.True(equality);\n\n        // Arrange, Act, Assert \n        l = new(new(\"host\", 333, \"ws\"), 4);\n        boxed = (object)l;\n        equality = Lease.Null.Equals(boxed);\n        Assert.False(equality);\n\n        // Arrange, Act, Assert\n        string s = \"not Lease\";\n        boxed = (object)s;\n        equality = l.Equals(boxed);\n        Assert.False(equality);\n    }\n\n    [Fact]\n    public void Equals_Lease()\n    {\n        // Arrange, Act, Assert : false\n        Lease l = new(new(\"host\", 333, \"ws\"), 4);\n        Lease other = Lease.Null;\n        bool equality = l.Equals(other);\n        Assert.False(equality);\n\n        // Arrange, Act, Assert : true\n        equality = Lease.Null.Equals(other);\n        Assert.True(equality);\n    }\n\n    [Fact]\n    public void Op_Inequality_Lease_Lease()\n    {\n        // Arrange, Act, Assert : true\n        Lease x = new(new(\"host\", 333, \"ws\"), 4);\n        Lease y = Lease.Null;\n        bool equality = x != y;\n        Assert.True(equality);\n\n        // Arrange, Act, Assert : false\n        equality = Lease.Null != y;\n        Assert.False(equality);\n    }\n\n    [Fact]\n    public void Op_Inequality_ServiceHostAndPort_Lease()\n    {\n        // Arrange, Act, Assert : false\n        ServiceHostAndPort h = new(\"host\", 333, \"ws\");\n        Lease l = new(h, 1);\n        bool equality = h != l;\n        Assert.False(equality);\n\n        // Arrange, Act, Assert : true\n        equality = h != Lease.Null;\n        Assert.True(equality);\n    }\n\n    [Fact]\n    public void Op_Inequality_Lease_ServiceHostAndPort()\n    {\n        // Arrange, Act, Assert : false\n        ServiceHostAndPort h = new(\"host\", 333, \"ws\");\n        Lease l = new(h, 1);\n        bool equality = l != h;\n        Assert.False(equality);\n\n        // Arrange, Act, Assert : true\n        equality = Lease.Null != h;\n        Assert.True(equality);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.ServiceDiscovery.Providers;\r\nusing System.Reflection;\n\r\nnamespace Ocelot.UnitTests.LoadBalancer;\r\n\r\npublic class LeastConnectionCreatorTests : UnitTest\r\n{\r\n    private readonly LeastConnectionCreator _creator;\r\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\r\n\r\n    public LeastConnectionCreatorTests()\r\n    {\r\n        _creator = new();\r\n        _serviceProvider = new();\r\n    }\n\n    [Theory]\r\n    [InlineData(false)]\r\n    [InlineData(true)]\r\n    public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName)\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(isNullServiceName ? null : \"myService\")\r\n            .WithLoadBalancerKey(\"key\")\r\n            .Build();\r\n\r\n        // Act\r\n        var response = _creator.Create(route, _serviceProvider.Object);\r\n\r\n        // Assert\r\n        response.Data.ShouldBeOfType<LeastConnection>();\r\n        var balancer = response.Data as LeastConnection;\r\n        var field = balancer.GetType().GetField(\"_serviceName\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        var serviceName = field.GetValue(balancer) as string;\r\n        serviceName.ShouldBe(isNullServiceName ? \"key\" : \"myService\");\r\n    }\n\n    [Fact]\r\n    public void Should_return_expected_name()\r\n    {\r\n        // Arrange, Act, Assert\r\n        _creator.Type.ShouldBe(nameof(LeastConnection));\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Values;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.LoadBalancer;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LeastConnectionTests : UnitTest\n{\n    private LeastConnection _leastConnection;\n    private readonly Random _random;\n    private readonly DefaultHttpContext _httpContext;\n\n    public LeastConnectionTests()\n    {\n        _httpContext = new();\n        _random = new();\n    }\n\n    [Fact]\n    public async Task Should_be_able_to_lease_and_release_concurrently()\n    {\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        var tasks = new Task[100];\n        for (var i = 0; i < tasks.Length; i++)\n        {\n            tasks[i] = LeaseDelayAndRelease();\n        }\n\n        await Task.WhenAll(tasks);\n    }\n\n    [Fact]\n    public async Task Should_handle_service_returning_to_available()\n    {\n        const string ServiceName = \"products\";\n\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        var hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortOne.Data.DownstreamHost.ShouldBe(\"127.0.0.1\");\n        var hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortTwo.Data.DownstreamHost.ShouldBe(\"127.0.0.2\");\n        _leastConnection.Release(hostAndPortOne.Data);\n        _leastConnection.Release(hostAndPortTwo.Data);\n\n        availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortOne.Data.DownstreamHost.ShouldBe(\"127.0.0.1\");\n        hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortTwo.Data.DownstreamHost.ShouldBe(\"127.0.0.1\");\n        _leastConnection.Release(hostAndPortOne.Data);\n        _leastConnection.Release(hostAndPortTwo.Data);\n\n        availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortOne.Data.DownstreamHost.ShouldBe(\"127.0.0.1\");\n        hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext);\n        hostAndPortTwo.Data.DownstreamHost.ShouldBe(\"127.0.0.2\");\n        _leastConnection.Release(hostAndPortOne.Data);\n        _leastConnection.Release(hostAndPortTwo.Data);\n    }\n\n    private async Task LeaseDelayAndRelease()\n    {\n        var hostAndPort = await _leastConnection.LeaseAsync(_httpContext);\n        await Task.Delay(_random.Next(1, 100));\n        _leastConnection.Release(hostAndPort.Data);\n    }\n\n    [Fact]\n    public async Task Should_get_next_url()\n    {\n        // Arrange\n        const string ServiceName = \"products\";\n        var hostAndPort = new ServiceHostAndPort(\"localhost\", 80);\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, hostAndPort, string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        // Act\n        var result = await _leastConnection.LeaseAsync(_httpContext);\n\n        // Assert\n        result.Data.DownstreamHost.ShouldBe(hostAndPort.DownstreamHost);\n        result.Data.DownstreamPort.ShouldBe(hostAndPort.DownstreamPort);\n    }\n\n    [Fact]\n    public async Task Should_serve_from_service_with_least_connections()\n    {\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.3\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        var response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost);\n    }\n\n    [Fact]\n    public async Task Should_build_connections_per_service()\n    {\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        var response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n    }\n\n    [Fact]\n    public async Task Should_release_connection()\n    {\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.2\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        var response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n\n        //release this so 2 should have 1 connection and we should get 2 back as our next host and port\n        _leastConnection.Release(availableServices[1].HostAndPort);\n\n        response = await _leastConnection.LeaseAsync(_httpContext);\n\n        response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost);\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_services_are_null()\n    {\n        // Arrange\n        const string ServiceName = \"products\";\n        var hostAndPort = new ServiceHostAndPort(\"localhost\", 80);\n        _leastConnection = new LeastConnection(() => Task.FromResult((List<Service>)null), ServiceName);\n\n        // Act\n        var result = await _leastConnection.LeaseAsync(_httpContext);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].ShouldBeOfType<ServicesAreNullError>();\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_services_are_empty()\n    {\n        // Arrange\n        const string ServiceName = \"products\";\n        _leastConnection = new LeastConnection(() => Task.FromResult(new List<Service>()), ServiceName);\n\n        // Act\n        var result = await _leastConnection.LeaseAsync(_httpContext);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].ShouldBeOfType<ServicesAreNullError>();\n    }\n\n    [Fact]\n    public async Task OnLeased()\n    {\n        // Arrange\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        var leastConnection = new TestLeastConnection(() => Task.FromResult(availableServices), ServiceName);\n\n        // Act\n        var result = await leastConnection.LeaseAsync(_httpContext);\n\n        // Assert\n        leastConnection.Events.ShouldNotBeEmpty();\n        var args = leastConnection.Events[0].ShouldNotBeNull();\n        args.Service.Name.ShouldBe(ServiceName);\n    }\n}\n\ninternal sealed class TestLeastConnection : LeastConnection, ILoadBalancer\n{\n    public readonly List<LeaseEventArgs> Events = new();\n    public TestLeastConnection(Func<Task<List<Service>>> services, string serviceName)\n        : base(services, serviceName) => Leased += Me_Leased;\n    private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LoadBalancerFactoryTests : UnitTest\n{\n    private readonly LoadBalancerFactory _factory;\n    private readonly Mock<IServiceDiscoveryProviderFactory> _serviceProviderFactory;\n    private readonly IEnumerable<ILoadBalancerCreator> _loadBalancerCreators;\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\n\n    public LoadBalancerFactoryTests()\n    {\n        _serviceProviderFactory = new Mock<IServiceDiscoveryProviderFactory>();\n        _serviceProvider = new Mock<IServiceDiscoveryProvider>();\n        _loadBalancerCreators = new ILoadBalancerCreator[]\n        {\n            new FakeLoadBalancerCreator<FakeLoadBalancerOne>(),\n            new FakeLoadBalancerCreator<FakeLoadBalancerTwo>(),\n            new FakeLoadBalancerCreator<FakeNoLoadBalancer>(nameof(NoLoadBalancer)),\n            new BrokenLoadBalancerCreator<BrokenLoadBalancer>(),\n        };\n        _factory = new LoadBalancerFactory(_serviceProviderFactory.Object, _loadBalancerCreators);\n    }\n\n    [Fact]\n    public void Should_return_no_load_balancer_by_default()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryReturns();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        result.Data.ShouldBeOfType<FakeNoLoadBalancer>();\n    }\n\n    [Fact]\n    public void Should_return_matching_load_balancer()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerTwo), string.Empty, 0))\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryReturns();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        result.Data.ShouldBeOfType<FakeLoadBalancerTwo>();\n    }\n\n    [Fact]\n    public void Should_return_error_response_if_cannot_find_load_balancer_creator()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(\"DoesntExistLoadBalancer\", string.Empty, 0))\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryReturns();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].Message.ShouldBe(\"Could not find load balancer creator for Type: DoesntExistLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name.\");\n    }\n\n    [Fact]\n    public void Should_return_error_response_if_creator_errors()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(BrokenLoadBalancer), string.Empty, 0))\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryReturns();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Should_call_service_provider()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0))\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryReturns();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        ThenTheServiceProviderIsCalledCorrectly();\n    }\n\n    [Fact]\n    public void Should_return_error_response_when_call_to_service_provider_fails()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0))\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var config = new ServiceProviderConfigurationBuilder().Build();\n        GivenTheServiceProviderFactoryFails();\n\n        // Act\n        var result = _factory.Get(route, config);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    private void GivenTheServiceProviderFactoryReturns()\n    {\n        _serviceProviderFactory\n            .Setup(x => x.Get(It.IsAny<ServiceProviderConfiguration>(), It.IsAny<DownstreamRoute>()))\n            .Returns(new OkResponse<IServiceDiscoveryProvider>(_serviceProvider.Object));\n    }\n\n    private void GivenTheServiceProviderFactoryFails()\n    {\n        _serviceProviderFactory\n            .Setup(x => x.Get(It.IsAny<ServiceProviderConfiguration>(), It.IsAny<DownstreamRoute>()))\n            .Returns(new ErrorResponse<IServiceDiscoveryProvider>(new CannotFindDataError(\"For tests\")));\n    }\n\n    private void ThenTheServiceProviderIsCalledCorrectly()\n    {\n        _serviceProviderFactory\n            .Verify(x => x.Get(It.IsAny<ServiceProviderConfiguration>(), It.IsAny<DownstreamRoute>()), Times.Once);\n    }\n\n    private class FakeLoadBalancerCreator<T> : ILoadBalancerCreator\n        where T : ILoadBalancer, new()\n    {\n        public FakeLoadBalancerCreator() => Type = typeof(T).Name;\n        public FakeLoadBalancerCreator(string type) => Type = type;\n        public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) => new OkResponse<ILoadBalancer>(new T());\n        public string Type { get; }\n    }\n\n    private class BrokenLoadBalancerCreator<T> : ILoadBalancerCreator\n        where T : ILoadBalancer, new()\n    {\n        public BrokenLoadBalancerCreator() => Type = typeof(T).Name;\n        public Response<ILoadBalancer> Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider)\n            => new ErrorResponse<ILoadBalancer>(new InvokingLoadBalancerCreatorError(new Exception()));\n        public string Type { get; }\n    }\n\n    private class FakeLoadBalancerOne : ILoadBalancer\n    {\n        public string Type => nameof(FakeLoadBalancerOne);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n\n    private class FakeLoadBalancerTwo : ILoadBalancer\n    {\n        public string Type => nameof(FakeLoadBalancerTwo);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n\n    private class FakeNoLoadBalancer : ILoadBalancer\n    {\n        public string Type => nameof(FakeNoLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n\n    private class BrokenLoadBalancer : ILoadBalancer\n    {\n        public string Type => nameof(BrokenLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LoadBalancerHouseTests : UnitTest\n{\n    private readonly LoadBalancerHouse _house;\n    private readonly Mock<ILoadBalancerFactory> _factory;\n    private readonly ServiceProviderConfiguration _serviceProviderConfig;\n\n    public LoadBalancerHouseTests()\n    {\n        _factory = new Mock<ILoadBalancerFactory>();\n        _house = new LoadBalancerHouse(_factory.Object);\n        _serviceProviderConfig = new()\n        {\n            Type = \"myType\",\n            Scheme = \"myScheme\",\n            Host = \"myHost\",\n            Port = 123,\n            ConfigurationKey = \"configKey\",\n        };\n    }\n\n    [Fact]\n    public void Should_store_load_balancer_on_first_request()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer));\n\n        // Act\n        var result = _house.Get(route, _serviceProviderConfig);\n\n        // Assert: Then It Is Added\n        result.IsError.ShouldBe(false);\n        result.ShouldBeOfType<OkResponse<ILoadBalancer>>();\n        result.Data.ShouldBe(loadBalancer);\n        _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once);\n    }\n\n    [Fact]\n    public void Should_not_store_load_balancer_on_second_request()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0))\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer));\n\n        // Act\n        var result = _house.Get(route, _serviceProviderConfig);\n\n        // Assert\n        result.Data.ShouldBe(loadBalancer);\n        _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once);\n    }\n\n    [Fact]\n    public void Should_store_load_balancers_by_key()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0))\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var route2 = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeRoundRobinLoadBalancer), string.Empty, 0))\n            .WithLoadBalancerKey(\"testtwo\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        var loadBalancer2 = new FakeRoundRobinLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer));\n        _factory.Setup(x => x.Get(route2, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer2));\n\n        // Act, Assert\n        var result = _house.Get(route, _serviceProviderConfig);\n        result.Data.ShouldBeOfType<FakeLoadBalancer>();\n\n        // Act, Assert\n        result = _house.Get(route2, _serviceProviderConfig);\n        result.Data.ShouldBeOfType<FakeRoundRobinLoadBalancer>();\n    }\n\n    [Fact]\n    public void Should_return_error_if_exception()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder().Build();\n\n        // Act\n        var result = _house.Get(route, _serviceProviderConfig);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.Errors[0].ShouldBeOfType<UnableToFindLoadBalancerError>();\n    }\n\n    [Fact]\n    public void Should_get_new_load_balancer_if_route_load_balancer_has_changed()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0))\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var route2 = new DownstreamRouteBuilder()\n            .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(LeastConnection), string.Empty, 0))\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer));\n\n        // Act, Assert\n        var result = _house.Get(route, _serviceProviderConfig);\n        result.Data.ShouldBeOfType<FakeLoadBalancer>();\n        _factory.Setup(x => x.Get(route2, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(new LeastConnection(null, null)));\n\n        // Act, Assert\n        result = _house.Get(route2, _serviceProviderConfig);\n        result.Data.ShouldBeOfType<LeastConnection>();\n    }\n\n    [Fact]\n    public void GetResponse_IsError()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig))\n            .Returns(new ErrorResponse<ILoadBalancer>(new CouldNotFindLoadBalancerCreatorError($\"Could not find load balancer creator for Type: FakeLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name.\")));\n\n        // Act\n        var result = _house.Get(route, _serviceProviderConfig);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n        result.ShouldBeOfType<ErrorResponse<ILoadBalancer>>();\n        result.Data.ShouldBeNull();\n        _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once);\n        result.Errors.Single().ShouldBeOfType<CouldNotFindLoadBalancerCreatorError>();\n    }\n\n    [Fact]\n    public void TypesMismatched_ShouldReturnError()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(\"test\")\n            .Build();\n        var loadBalancer = new FakeLoadBalancer();\n        _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse<ILoadBalancer>(loadBalancer));\n\n        // Other route has the same LoadBalancerKey but types are different\n        var route2 = new DownstreamRouteBuilder()\n            .WithLoadBalancerKey(route.LoadBalancerKey)\n            .WithLoadBalancerOptions(new() { Type = \"bla-bla\" })\n            .Build();\n        _factory.Setup(x => x.Get(route2, _serviceProviderConfig))\n            .Returns(new ErrorResponse<ILoadBalancer>(new CouldNotFindLoadBalancerCreatorError($\"Could not find load balancer creator for Type: {route2.LoadBalancerOptions.Type}, please check your config specified the correct load balancer and that you have registered a class with the same name.\")));\n\n        // Act\n        var result = _house.Get(route2, _serviceProviderConfig);\n\n        // Assert: Then It Is Added\n        result.IsError.ShouldBeTrue();\n        result.ShouldBeOfType<ErrorResponse<ILoadBalancer>>();\n        result.Data.ShouldBeNull();\n        _factory.Verify(x => x.Get(route2, _serviceProviderConfig), Times.Once);\n        result.Errors.Single().ShouldBeOfType<CouldNotFindLoadBalancerCreatorError>()\n            .Message.ShouldBe(\"Could not find load balancer creator for Type: bla-bla, please check your config specified the correct load balancer and that you have registered a class with the same name.\");\n    }\n\n    private class FakeLoadBalancer : ILoadBalancer\n    {\n        public string Type => nameof(FakeLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n\n    private class FakeRoundRobinLoadBalancer : ILoadBalancer\n    {\n        public string Type => nameof(FakeRoundRobinLoadBalancer);\n        public Task<Response<ServiceHostAndPort>> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException();\n        public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Errors;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LoadBalancerMiddlewareTests : UnitTest\n{\n    private readonly Mock<ILoadBalancerHouse> _loadBalancerHouse;\n    private readonly Mock<ILoadBalancer> _loadBalancer;\n    private ServiceHostAndPort _hostAndPort;\n    private ErrorResponse<ILoadBalancer> _getLoadBalancerHouseError;\n    private ErrorResponse<ServiceHostAndPort> _getHostAndPortError;\n    private readonly HttpRequestMessage _downstreamRequest;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private LoadBalancingMiddleware _middleware;\n    private RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public LoadBalancerMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _loadBalancer = new Mock<ILoadBalancer>();\n        _loadBalancerHouse = new Mock<ILoadBalancerHouse>();\n        _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, \"http://test.com/\");\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<LoadBalancingMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n\n        _loadBalancerHouse.Setup(x => x.Get(It.IsAny<DownstreamRoute>(), It.IsAny<ServiceProviderConfiguration>()))\n            .Returns(new OkResponse<ILoadBalancer>(_loadBalancer.Object));\n    }\n\n    private void Arrange()\n    {\n        var downstreamRoute = new DownstreamRouteBuilder()\n            .WithUpstreamHttpMethod([HttpMethods.Get])\n            .Build();\n        var serviceProviderConfig = new ServiceProviderConfigurationBuilder()\n            .Build();\n        GivenTheDownStreamUrlIs(\"http://my.url/abc?q=123\");\n        GivenTheConfigurationIs(serviceProviderConfig);\n        GivenTheDownStreamRouteIs(downstreamRoute, new List<PlaceholderNameAndValue>());\n    }\n\n    [Fact]\n    public async Task Should_call_scoped_data_repository_correctly()\n    {\n        Arrange();\n\n        // Arrange: Given The Load Balancer Returns\n        _hostAndPort = new ServiceHostAndPort(\"127.0.0.1\", 80);\n        _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(_hostAndPort));\n\n        // Act\n        _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object);\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(\"http://127.0.0.1:80/abc?q=123\");\n    }\n\n    [Fact]\n    public async Task Should_set_pipeline_error_if_cannot_get_load_balancer()\n    {\n        Arrange();\n\n        // Arrange: Given The Load Balancer House Returns An Error\n        _getLoadBalancerHouseError = new ErrorResponse<ILoadBalancer>(new List<Error>\n        {\n            new UnableToFindLoadBalancerError(\"unabe to find load balancer for bah\"),\n        });\n        _loadBalancerHouse.Setup(x => x.Get(It.IsAny<DownstreamRoute>(), It.IsAny<ServiceProviderConfiguration>()))\n            .Returns(_getLoadBalancerHouseError);\n\n        // Act\n        _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object);\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0);\n        _httpContext.Items.Errors().ShouldBe(_getLoadBalancerHouseError.Errors);\n    }\n\n    [Fact]\n    public async Task Should_set_pipeline_error_if_cannot_get_least()\n    {\n        Arrange();\n\n        // Arrange: Given The Load Balancer Returns An Error\n        _getHostAndPortError = new ErrorResponse<ServiceHostAndPort>(new List<Error> { new ServicesAreNullError(\"services were null for bah\") });\n        _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n           .ReturnsAsync(_getHostAndPortError);\n\n        // Act\n        _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object);\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0);\n        _httpContext.Items.Errors().ShouldBe(_getHostAndPortError.Errors);\n    }\n\n    [Fact]\n    public async Task Should_set_scheme()\n    {\n        Arrange();\n\n        // Arrange: Given The Load Balancer Returns Ok\n        _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(new ServiceHostAndPort(\"abc\", 123, \"https\")));\n\n        // Act\n        _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object);\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.DownstreamRequest().Host.ShouldBeEquivalentTo(\"abc\");\n        _httpContext.Items.DownstreamRequest().Port.ShouldBeEquivalentTo(123);\n        _httpContext.Items.DownstreamRequest().Scheme.ShouldBeEquivalentTo(\"https\");\n    }\n\n    [Fact]\n    public async Task ShouldNot_LogDebug_WhenNextMiddlewareThrownException()\n    {\n        Arrange();\n        _next = (context) => throw new NotImplementedException(\"NextMiddleware\");\n\n        // Arrange: Given The Load Balancer Returns Ok\n        _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny<HttpContext>()))\n            .ReturnsAsync(new OkResponse<ServiceHostAndPort>(new ServiceHostAndPort(\"abc\", 123, \"https\")));\n        _logger.Setup(x => x.LogDebug(It.IsAny<string>())).Verifiable();\n\n        // Act\n        _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object);\n        var action = () => _middleware.Invoke(_httpContext);\n        var ex = await action.ShouldThrowAsync<NotImplementedException>();\n        ex.Message.ShouldBe(\"NextMiddleware\");\n        _logger.Verify(x => x.LogDebug(It.IsAny<string>()), Times.Never);\n    }\n\n    private void GivenTheConfigurationIs(ServiceProviderConfiguration config)\n    {\n        var configuration = new InternalConfiguration()\n        {\n            ServiceProviderConfiguration = config,\n        };\n        _httpContext.Items.SetIInternalConfiguration(configuration);\n    }\n\n    private void GivenTheDownStreamUrlIs(string downstreamUrl)\n    {\n        _downstreamRequest.RequestUri = new Uri(downstreamUrl);\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest));\n    }\n\n    private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute, List<PlaceholderNameAndValue> placeholder)\n    {\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(placeholder);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsCreatorTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.LoadBalancer.Balancers;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LoadBalancerOptionsCreatorTests : UnitTest\n{\n    private readonly LoadBalancerOptionsCreator _creator = new();\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Should_create(bool isNull)\n    {\n        // Arrange\n        var options = isNull ? null :\n            new FileLoadBalancerOptions\n            {\n                Type = \"test\",\n                Key = \"west\",\n                Expiry = 1,\n            };\n\n        // Act\n        var result = _creator.Create(options);\n\n        // Assert\n        result.Type.ShouldBe(isNull ? \"NoLoadBalancer\" : \"test\");\n        result.Key.ShouldBe(isNull ? null : \"west\");\n        result.ExpiryInMs.ShouldBe(isNull ? 0 : 1);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void Create_FromRoute_NullChecks()\n    {\n        // Arrange, Act, Assert\n        FileRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void Create_FromRoute()\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            LoadBalancerOptions = new()\n            {\n                Key = \"route\",\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            LoadBalancerOptions = new(\"global\"),\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.Equal(\"global\", actual.Type);\n        Assert.Equal(\"route\", actual.Key);\n        Assert.Equal(0, actual.ExpiryInMs);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void Create_FromDynamicRoute_NullChecks()\n    {\n        // Arrange, Act, Assert\n        FileDynamicRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void Create_FromDynamicRoute()\n    {\n        // Arrange\n        FileDynamicRoute route = new()\n        {\n            LoadBalancerOptions = new()\n            {\n                Key = \"route\",\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            LoadBalancerOptions = new(\"global\"),\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.Equal(\"global\", actual.Type);\n        Assert.Equal(\"route\", actual.Key);\n        Assert.Equal(0, actual.ExpiryInMs);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void CreateProtected_NullCheck()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        IRouteGrouping grouping = null;\n        FileLoadBalancerOptions options = null;\n        FileGlobalLoadBalancerOptions globalOptions = null;\n\n        // Act\n        var wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [grouping, options, globalOptions]));\n\n        // Assert\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        var actual = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(grouping), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void CreateProtected()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        FileDynamicRoute route = new() { Key = \"r1\" };\n        FileLoadBalancerOptions options = null;\n        FileGlobalLoadBalancerOptions globalOptions = new()\n        {\n            RouteKeys = null,\n            Type = \"global\",\n            Key = \"global\",\n            Expiry = 1,\n        };\n\n        // Act, Assert\n        var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n        Assert.Equal(\"global\", actual.Type);\n        Assert.Equal(\"global\", actual.Key);\n        Assert.Equal(1, actual.ExpiryInMs);\n\n        // Arrange 2\n        options = new()\n        {\n            Type = \"route\",\n            Key = \"route\",\n            Expiry = 3,\n        };\n        globalOptions.RouteKeys = [\"?\"];\n\n        // Act, Assert 2\n        actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n        Assert.Equal(\"route\", actual.Type);\n        Assert.Equal(\"route\", actual.Key);\n        Assert.Equal(3, actual.ExpiryInMs);\n\n        globalOptions.RouteKeys = [\"r1\"];\n        actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n        Assert.Equal(\"route\", actual.Type);\n        Assert.Equal(\"route\", actual.Key);\n        Assert.Equal(3, actual.ExpiryInMs);\n\n        globalOptions = null;\n        actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n        Assert.Equal(\"route\", actual.Type);\n        Assert.Equal(\"route\", actual.Key);\n        Assert.Equal(3, actual.ExpiryInMs);\n\n        // Arrange 3\n        options.Key = null;\n        globalOptions = new()\n        {\n            RouteKeys = null,\n            Type = \"global\",\n            Key = \"global\",\n            Expiry = 1,\n        };\n        actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n        Assert.Equal(\"route\", actual.Type);\n        Assert.Equal(\"global\", actual.Key);\n        Assert.Equal(3, actual.ExpiryInMs);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    public void CreateProtected_NoOptions()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(\"Create\", BindingFlags.Instance | BindingFlags.NonPublic);\n        FileDynamicRoute route = new();\n        FileLoadBalancerOptions options = null;\n        FileGlobalLoadBalancerOptions globalOptions = null;\n\n        // Act\n        var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]);\n\n        // Assert\n        Assert.Equal(nameof(NoLoadBalancer), actual.Type);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n\n    public void Merge_NullCheck()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic);\n        FileLoadBalancerOptions options = null;\n        FileLoadBalancerOptions globalOptions = null;\n\n        // Act, Assert 1\n        var wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [null, globalOptions]));\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        var actual = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(options), actual.ParamName);\n\n        // Act, Assert 2\n        options = new();\n        wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [options, null]));\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        actual = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(globalOptions), actual.ParamName);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2324\")]\n    [Trait(\"Feat\", \"2319\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Merge(bool isDef)\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic);\n        FileLoadBalancerOptions options = new()\n        {\n            Type = isDef ? string.Empty : \"route\",\n            Key = isDef ? string.Empty : \"route\",\n            Expiry = isDef ? null : 1,\n        };\n        FileLoadBalancerOptions globalOptions = new(\"global\")\n        {\n            Key = \"global\",\n            Expiry = 3,\n        };\n\n        // Act\n        var actual = (LoadBalancerOptions)method.Invoke(_creator, [options, globalOptions]);\n\n        // Assert\n        Assert.Equal(isDef ? \"global\" : \"route\", actual.Type);\n        Assert.Equal(isDef ? \"global\" : \"route\", actual.Key);\n        Assert.Equal(isDef ? 3 : 1, actual.ExpiryInMs);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\nusing Ocelot.LoadBalancer.Balancers;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class LoadBalancerOptionsTests\n{\n    [Fact]\n    public void Ctor_ShouldDefaultToNoLoadBalancer()\n    {\n        // Arrange, Act\n        LoadBalancerOptions options = new();\n        LoadBalancerOptions options2 = new(default, default, default);\n        LoadBalancerOptions options3 = new(new FileLoadBalancerOptions());\n\n        // Assert\n        Assert.Equal(nameof(NoLoadBalancer), options.Type);\n        Assert.Equal(nameof(NoLoadBalancer), options2.Type);\n        Assert.Equal(nameof(NoLoadBalancer), options3.Type);\n    }\n\n    [Fact]\n    public void Ctor_Parameterless()\n    {\n        // Arrange, Act\n        LoadBalancerOptions actual = new();\n\n        // Assert\n        Assert.Equal(nameof(NoLoadBalancer), actual.Type);\n        Assert.Null(actual.Key);\n        Assert.Equal(0, actual.ExpiryInMs);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2324\")]\n    public void Ctor_CopyingFrom_FileLoadBalancerOptions()\n    {\n        // Arrange\n        FileLoadBalancerOptions from = new()\n        {\n            Type = \"Balancer\",\n            Key = \"BalancerKey\",\n            Expiry = 3,\n        };\n\n        // Act\n        LoadBalancerOptions actual = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equal(\"Balancer\", actual.Type);\n        Assert.Equal(\"BalancerKey\", actual.Key);\n        Assert.Equal(3, actual.ExpiryInMs);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2324\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Ctor_Initialization3Params(bool isDef)\n    {\n        // Arrange\n        FileLoadBalancerOptions from = new()\n        {\n            Type = isDef ? string.Empty : \"TestBalancer\",\n            Key = isDef ? string.Empty : \"Bla-Bla\",\n            Expiry = isDef ? null : 3,\n        };\n\n        // Act\n        LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry);\n\n        // Assert\n        Assert.Equal(isDef ? nameof(NoLoadBalancer) : \"TestBalancer\", actual.Type);\n        Assert.Equal(isDef ? string.Empty : \"Bla-Bla\", actual.Key);\n        Assert.Equal(isDef ? 0 : 3, actual.ExpiryInMs);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2324\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Ctor_Initialization_CookieStickySessions(bool isDef)\n    {\n        // Arrange\n        FileLoadBalancerOptions from = new()\n        {\n            Type = nameof(CookieStickySessions),\n            Key = isDef ? string.Empty : \"Key\",\n            Expiry = isDef ? null : 3,\n        };\n\n        // Act\n        LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry);\n\n        // Assert\n        Assert.Equal(\"CookieStickySessions\", actual.Type);\n        Assert.Equal(isDef ? \".AspNetCore.Session\" : \"Key\", actual.Key);\n        Assert.Equal(isDef ? 1200000 : 3, actual.ExpiryInMs);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.ServiceDiscovery.Providers;\n\r\nnamespace Ocelot.UnitTests.LoadBalancer;\r\n\r\npublic class NoLoadBalancerCreatorTests : UnitTest\r\n{\r\n    private readonly NoLoadBalancerCreator _creator;\r\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\r\n\r\n    public NoLoadBalancerCreatorTests()\r\n    {\r\n        _creator = new NoLoadBalancerCreator();\r\n        _serviceProvider = new Mock<IServiceDiscoveryProvider>();\r\n    }\n\n    [Fact]\r\n    public void Should_return_instance_of_expected_load_balancer_type()\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder().Build();\r\n\r\n        // Act\r\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\r\n\r\n        // Assert\r\n        loadBalancer.Data.ShouldBeOfType<NoLoadBalancer>();\r\n    }\n\n    [Fact]\r\n    public void Should_return_expected_name()\r\n    {\r\n        // Arrange, Act, Assert\r\n        _creator.Type.ShouldBe(nameof(NoLoadBalancer));\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.Responses;\nusing Ocelot.Values;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class NoLoadBalancerTests : UnitTest\n{\n    private readonly List<Service> _services;\n    private NoLoadBalancer _loadBalancer;\n    private Response<ServiceHostAndPort> _result;\n\n    public NoLoadBalancerTests()\n    {\n        _services = new List<Service>();\n        _loadBalancer = new NoLoadBalancer(() => Task.FromResult(_services));\n    }\n\n    [Fact]\n    public async Task Should_return_host_and_port()\n    {\n        // Arrange\n        var hostAndPort = new ServiceHostAndPort(\"127.0.0.1\", 80);\n        var services = new List<Service>\n        {\n            new(\"product\", hostAndPort, string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        _services.AddRange(services);\n\n        // Act\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n\n        // Assert\n        _result.Data.ShouldBe(hostAndPort);\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_no_services()\n    {\n        // Arrange, Act\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n\n        // Assert\n        _result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_no_services_then_when_services_available_return_host_and_port()\n    {\n        // Arrange\n        var hostAndPort = new ServiceHostAndPort(\"127.0.0.1\", 80);\n\n        var services = new List<Service>\n        {\n            new(\"product\", hostAndPort, string.Empty, string.Empty, Array.Empty<string>()),\n        };\n\n        // Act, Assert\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n        _result.IsError.ShouldBeTrue();\n        _services.AddRange(services);\n\n        // Act, Assert\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n        _result.Data.ShouldBe(hostAndPort);\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_null_services()\n    {\n        // Arrange\n        _loadBalancer = new NoLoadBalancer(() => Task.FromResult((List<Service>)null));\n\n        // Act\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n\n        // Assert\n        _result.IsError.ShouldBeTrue();\n    }\n\n    [Fact]\n    public async Task Release()\n    {\n        // Arrange\n        var hostAndPort = new ServiceHostAndPort(\"127.0.0.1\", 80);\n        var services = new List<Service>\n        {\n            new(\"product\", hostAndPort, string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        _services.AddRange(services);\n        _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext());\n\n        // Act\n        _loadBalancer.Release(hostAndPort);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration.Builder;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Creators;\nusing Ocelot.ServiceDiscovery.Providers;\nusing System.Reflection;\n\r\nnamespace Ocelot.UnitTests.LoadBalancer;\r\n\r\npublic class RoundRobinCreatorTests : UnitTest\r\n{\r\n    private readonly RoundRobinCreator _creator;\r\n    private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;\r\n\r\n    public RoundRobinCreatorTests()\r\n    {\r\n        _creator = new RoundRobinCreator();\r\n        _serviceProvider = new Mock<IServiceDiscoveryProvider>();\r\n    }\n\n    [Theory]\r\n    [InlineData(false)]\r\n    [InlineData(true)]\r\n    public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName)\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(isNullServiceName ? null : \"myService\")\r\n            .WithLoadBalancerKey(\"key\")\r\n            .Build();\r\n\r\n        // Act\r\n        var loadBalancer = _creator.Create(route, _serviceProvider.Object);\r\n\r\n        // Assert\r\n        loadBalancer.Data.ShouldBeOfType<RoundRobin>();\r\n        var balancer = loadBalancer.Data as RoundRobin;\r\n        var field = balancer.GetType().GetField(\"_serviceName\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        var serviceName = field.GetValue(balancer) as string;\r\n        serviceName.ShouldBe(isNullServiceName ? \"key\" : \"myService\");\r\n    }\n\n    [Fact]\r\n    public void Should_return_expected_name()\r\n    {\r\n        // Arrange, Act, Assert\r\n        _creator.Type.ShouldBe(nameof(RoundRobin));\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.LoadBalancer;\nusing Ocelot.LoadBalancer.Balancers;\nusing Ocelot.LoadBalancer.Errors;\nusing Ocelot.LoadBalancer.Interfaces;\nusing Ocelot.Responses;\nusing Ocelot.Values;\nusing System.Diagnostics;\nusing System.Reflection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.LoadBalancer;\n\npublic class RoundRobinTests : UnitTest\n{\n    private readonly DefaultHttpContext _httpContext = new();\n\n    [Fact]\n    public async Task Lease_LoopThroughIndexRangeOnce_ShouldGetNextAddress()\n    {\n        // Arrange\n        var services = GivenServices();\n        var roundRobin = GivenLoadBalancer(services);\n\n        // Act\n        var response0 = await roundRobin.LeaseAsync(_httpContext);\n        var response1 = await roundRobin.LeaseAsync(_httpContext);\n        var response2 = await roundRobin.LeaseAsync(_httpContext);\n\n        // Assert\n        response0.Data.ShouldNotBeNull().ShouldBe(services[0].HostAndPort);\n        response1.Data.ShouldNotBeNull().ShouldBe(services[1].HostAndPort);\n        response2.Data.ShouldNotBeNull().ShouldBe(services[2].HostAndPort);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"336\")]\n    public async Task Lease_LoopThroughIndexRangeIndefinitelyButOneSecond_ShouldGoBackToFirstAddressAfterFinishedLast()\n    {\n        // Arrange\n        var services = GivenServices();\n        var roundRobin = GivenLoadBalancer(services);\n        var stopWatch = Stopwatch.StartNew();\n        while (stopWatch.ElapsedMilliseconds < 1000)\n        {\n            // Act\n            var response0 = await roundRobin.LeaseAsync(_httpContext);\n            var response1 = await roundRobin.LeaseAsync(_httpContext);\n            var response2 = await roundRobin.LeaseAsync(_httpContext);\n\n            // Assert\n            response0.Data.ShouldNotBeNull().ShouldBe(services[0].HostAndPort);\n            response1.Data.ShouldNotBeNull().ShouldBe(services[1].HostAndPort);\n            response2.Data.ShouldNotBeNull().ShouldBe(services[2].HostAndPort);\n        }\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2110\")]\n    public async Task Lease_SelectedServiceIsNull_ShouldReturnError()\n    {\n        // Arrange\n        var invalidServices = new List<Service> { null };\n        var roundRobin = GivenLoadBalancer(invalidServices);\n\n        // Act\n        var response = await roundRobin.LeaseAsync(_httpContext);\n\n        // Assert: Then ServicesAreNullError Is Returned\n        response.ShouldNotBeNull().Data.ShouldBeNull();\n        response.IsError.ShouldBeTrue();\n        response.Errors[0].ShouldBeOfType<ServicesAreNullError>();\n    }\n\n    //[InlineData(1, 10)]\n    //[InlineData(2, 50)]\n    //[InlineData(3, 50)]\n    //[InlineData(4, 50)]\n    //[InlineData(5, 50)]\n    //[InlineData(3, 100)]\n    //[InlineData(4, 100)]\n    //[InlineData(7, 100)]\n    [InlineData(3, 100)]\n    [Theory]\n    [Trait(\"Feat\", \"2110\")]\n    public void Lease_LoopThroughIndexRangeIndefinitelyUnderHighLoad_ShouldDistributeIndexValuesUniformly(int totalServices, int totalThreads)\n    {\n        // Arrange\n        const bool ReturnServicesNotImmediately = false;\n        var services = GivenServices(totalServices);\n        var roundRobin = GivenLoadBalancer(services, ReturnServicesNotImmediately);\n        int bottom = totalThreads / totalServices,\n            top = totalThreads - (bottom * totalServices) + bottom;\n\n        // Act\n        var responses = WhenICallLeaseFromMultipleThreads(roundRobin, totalThreads);\n        var counters = CountServices(services, responses);\n\n        // Assert\n        responses.ShouldNotBeNull();\n        responses.Length.ShouldBe(totalThreads);\n\n        var message = $\"All values are [{string.Join(',', counters)}]\";\n        counters.Sum().ShouldBe(totalThreads, message);\n\n        message = $\"{nameof(bottom)}: {bottom}\\n\\t{nameof(top)}: {top}\\n\\tAll values are [{string.Join(',', counters)}]\";\n        counters.ShouldAllBe(counter => bottom <= counter && counter <= top, message);\n    }\n\n    [Fact]\n    public async Task OnLeased()\n    {\n        // Arrange\n        const string ServiceName = \"products\";\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        var roundRobin = new TestRoundRobin(() => Task.FromResult(availableServices), ServiceName);\n\n        // Act\n        var result = await roundRobin.LeaseAsync(_httpContext);\n\n        // Assert\n        Assert.NotEmpty(roundRobin.Events);\n        var args = roundRobin.Events[0];\n        Assert.NotNull(args);\n        Assert.Equal(ServiceName, args.Service.Name);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError(bool isNull)\n    {\n        // Arrange\n        List<Service> services = isNull ? null : GivenServices(0);\n        var roundRobin = GivenLoadBalancer(services);\n\n        // Act\n        var actual = await roundRobin.LeaseAsync(_httpContext);\n\n        // Assert\n        Assert.True(actual.IsError);\n        var error = actual.Errors[0];\n        Assert.IsType<ServicesAreEmptyError>(error);\n        Assert.Equal(\"There were no services in RoundRobin for 'LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError' during LeaseAsync operation!\", error.Message);\n    }\n\n    [Fact]\n    public async Task Release()\n    {\n        // Arrange\n        const string ServiceName = nameof(Release);\n        var availableServices = new List<Service>\n        {\n            new(ServiceName, new ServiceHostAndPort(\"127.0.0.1\", 80), string.Empty, string.Empty, Array.Empty<string>()),\n        };\n        var roundRobin = new RoundRobin(() => Task.FromResult(availableServices), ServiceName);\n        var response = await roundRobin.LeaseAsync(_httpContext);\n\n        // Act, Assert\n        roundRobin.Release(response.Data);\n    }\n\n    [Fact]\n    public void TryScanNext()\n    {\n        // Arrange\n        const int lastIndex = 3;\n        var method = typeof(RoundRobin).GetMethod(nameof(TryScanNext), BindingFlags.Instance | BindingFlags.NonPublic);\n        var field = typeof(RoundRobin).GetField(\"LastIndices\", BindingFlags.Static | BindingFlags.NonPublic);\n        List<Service> services = GivenServices(lastIndex);\n        var roundRobin = GivenLoadBalancer(services);\n        var lastIndices = field.GetValue(roundRobin) as Dictionary<string, int>;\n        lastIndices[nameof(TryScanNext)] = lastIndex;\n\n        // Act\n        // TryScanNext(Service[] readme, out Service next, out int index)\n        var readme = services.ToArray();\n        Service next = null;\n        int index = -1;\n        object[] parameters = [readme, next, index];\n        bool success = (bool)method.Invoke(roundRobin, parameters);\n\n        // Assert\n        Assert.True(success);\n        Assert.Equal(0, parameters[2]);\n        Assert.Equal(readme[0], parameters[1]);\n        Assert.Equal(1, lastIndices[nameof(TryScanNext)]);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Update_CanIncreaseConnections(bool increase)\n    {\n        var method = typeof(RoundRobin).GetMethod(\"Update\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var field = typeof(RoundRobin).GetField(\"_leasing\", BindingFlags.Instance | BindingFlags.NonPublic);\n        List<Service> services = GivenServices(1);\n        var roundRobin = GivenLoadBalancer(services);\n        Lease item = new(\n            services[0].HostAndPort,\n            increase ? 0 : 1);\n        var leasing = field.GetValue(roundRobin) as List<Lease>;\n        leasing.Add(item);\n\n        // Act\n        // int Update(ref Lease item, bool increase)\n        object[] parameters = [item, increase];\n        int index = (int)method.Invoke(roundRobin, parameters);\n\n        Lease actual = (Lease)parameters[0];\n        Assert.Equal(0, index);\n        Assert.Equal(increase ? 1 : 0, actual.Connections);\n    }\n\n    private static int[] CountServices(List<Service> services, Response<ServiceHostAndPort>[] responses)\n    {\n        var counters = new int[services.Count];\n        var firstPort = services[0].HostAndPort.DownstreamPort;\n        foreach (var response in responses)\n        {\n            var idx = response.Data.DownstreamPort - firstPort;\n            counters[idx]++;\n        }\n\n        return counters;\n    }\n\n    private Response<ServiceHostAndPort>[] WhenICallLeaseFromMultipleThreads(RoundRobin roundRobin, int times)\n    {\n        var tasks = new Task[times]; // allocate N-times threads as Task\n        var parallelResponses = new Response<ServiceHostAndPort>[times];\n        for (var i = 0; i < times; i++)\n        {\n            tasks[i] = GetParallelResponse(parallelResponses, roundRobin, i);\n        }\n\n        Task.WaitAll(tasks); // load by N-times threads\n        return parallelResponses;\n    }\n\n    private async Task GetParallelResponse(Response<ServiceHostAndPort>[] responses, RoundRobin roundRobin, int threadIndex)\n    {\n        responses[threadIndex] = await roundRobin.LeaseAsync(_httpContext);\n    }\n\n    private static List<Service> GivenServices(int total = 3, [CallerMemberName] string serviceName = null)\n    {\n        var list = new List<Service>(total);\n        for (int i = 1; i <= total; i++)\n        {\n            list.Add(new(serviceName, new ServiceHostAndPort(\"127.0.0.\" + i, 5000 + i), string.Empty, string.Empty, Array.Empty<string>()));\n        }\n\n        return list;\n    }\n\n    private static RoundRobin GivenLoadBalancer(List<Service> services, bool immediately = true, [CallerMemberName] string serviceName = null)\n    {\n        return new(\n            () =>\n            {\n                int leasingDelay = immediately ? 0 : Random.Shared.Next(5, 15);\n                Thread.Sleep(leasingDelay);\n                return Task.FromResult(services);\n            },\n            serviceName);\n    }\n}\n\ninternal sealed class TestRoundRobin : RoundRobin, ILoadBalancer\n{\n    public readonly List<LeaseEventArgs> Events = new();\n    public TestRoundRobin(Func<Task<List<Service>>> services, string serviceName)\n        : base(services, serviceName) => Leased += Me_Leased;\n    private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Logging;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class OcelotDiagnosticListenerTests : UnitTest\n{\n    private OcelotDiagnosticListener _listener;\n    private readonly Mock<IOcelotLoggerFactory> _factory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly IServiceCollection _serviceCollection;\n    private IServiceProvider _serviceProvider;\n    private readonly DefaultHttpContext _httpContext;\n\n    public OcelotDiagnosticListenerTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _factory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _serviceCollection = new ServiceCollection();\n        _serviceProvider = _serviceCollection.BuildServiceProvider(true);\n        _factory.Setup(x => x.CreateLogger<OcelotDiagnosticListener>()).Returns(_logger.Object);\n        _listener = new OcelotDiagnosticListener(_factory.Object, _serviceProvider);\n    }\n\n    [Fact]\n    public void Should_trace_middleware_started()\n    {\n        // Arrange\n        const string name = \"name\";\n\n        // Act\n        _listener.OnMiddlewareStarting(_httpContext, name);\n\n        // Assert\n        ThenTheLogIs($\"MiddlewareStarting: {name}; {_httpContext.Request.Path}\");\n    }\n\n    [Fact]\n    public void Should_trace_middleware_finished()\n    {\n        // Arrange\n        const string name = \"name\";\n\n        // Act\n        _listener.OnMiddlewareFinished(_httpContext, name);\n\n        // Assert\n        ThenTheLogIs($\"MiddlewareFinished: {name}; {_httpContext.Response.StatusCode}\");\n    }\n\n    [Fact]\n    public void Should_trace_middleware_exception()\n    {\n        // Arrange\n        const string name = \"name\";\n        var exception = new Exception(\"oh no\");\n\n        // Act\n        _listener.OnMiddlewareException(exception, name);\n\n        // Assert\n        ThenTheLogIs($\"MiddlewareException: {name}; {exception.Message};\");\n    }\n\n    [Fact]\n    public void Event()\n    {\n        // Arrange\n        var tracer = new Mock<IOcelotTracer>();\n        tracer.Setup(x => x.Event(It.IsAny<HttpContext>(), It.IsAny<string>()));\n        var method = _listener.GetType().GetMethod(nameof(Event), BindingFlags.Instance | BindingFlags.NonPublic);\n\n        // Act\n        method.Invoke(_listener, [_httpContext, TestID]);\n\n        // Assert 1 : _tracer is null\n        tracer.Verify(x => x.Event(It.IsAny<HttpContext>(), It.IsAny<string>()),\n            Times.Never);\n\n        // Scenario 2: _tracer is NOT null\n        _serviceCollection.AddSingleton<IOcelotTracer>(tracer.Object);\n        _serviceProvider = _serviceCollection.BuildServiceProvider(true);\n        _listener = new OcelotDiagnosticListener(_factory.Object, _serviceProvider);\n\n        // Act\n        method.Invoke(_listener, [_httpContext, TestID]);\n        tracer.Verify(x => x.Event(It.IsAny<HttpContext>(), It.IsAny<string>()),\n            Times.Once);\n    }\n\n    private void ThenTheLogIs(string expected)\n    {\n        _logger.Verify(x => x.LogTrace(It.Is<Func<string>>(c => c.Invoke() == expected)));\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/OcelotHttpTracingHandlerTests.cs",
    "content": "﻿using Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class OcelotHttpTracingHandlerTests : UnitTest\n{\n    private OcelotHttpTracingHandler handler;\n    private readonly Mock<IOcelotTracer> tracer = new();\n    private readonly Mock<IRequestScopedDataRepository> repo = new();\n    private readonly Mock<HttpMessageHandler> httpMessageHandler = new();\n    public OcelotHttpTracingHandlerTests()\n    {\n        handler = new(tracer.Object, repo.Object, httpMessageHandler.Object);\n    }\n\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange, Act\n        handler = new(tracer.Object, repo.Object, httpMessageHandler.Object);\n\n        // Assert\n        Assert.NotNull(handler.InnerHandler);\n        Assert.True(ReferenceEquals(httpMessageHandler.Object, handler.InnerHandler));\n    }\n\n    [Fact]\n    public void Ctor_NullChecks()\n    {\n        // Arrange, Act, Assert: argument 1\n        var ex = Assert.Throws<ArgumentNullException>(\n            () => handler = new(null, repo.Object, httpMessageHandler.Object));\n        Assert.Equal(nameof(tracer), ex.ParamName);\n\n        // Arrange, Act, Assert: argument 2\n        ex = Assert.Throws<ArgumentNullException>(\n            () => handler = new(tracer.Object, null, httpMessageHandler.Object));\n        Assert.Equal(nameof(repo), ex.ParamName);\n\n        // Arrange, Act, Assert: argument 3\n        handler = new(tracer.Object, repo.Object, null);\n        Assert.NotNull(handler.InnerHandler);\n        Assert.IsType<HttpClientHandler>(handler.InnerHandler);\n    }\n\n    [Fact]\n    public async Task SendAsync()\n    {\n        // Arrange\n        var sendAsync = handler.GetType().GetMethod(nameof(SendAsync), BindingFlags.Instance | BindingFlags.NonPublic);\n        HttpRequestMessage request = new();\n        CancellationToken token = CancellationToken.None;\n        HttpResponseMessage responseMessage = new();\n        tracer.Setup(x => x.SendAsync(request,\n            It.IsAny<Action<string>>(),\n            It.IsAny<Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>>(),\n            It.IsAny<CancellationToken>()))\n            .ReturnsAsync(responseMessage);\n        repo.Setup(x => x.Add<string>(It.IsAny<string>(), It.IsAny<string>()))\n            .Returns(new OkResponse());\n\n        // Act\n        var task = sendAsync.Invoke(handler, [request, token]) as Task<HttpResponseMessage>;\n        var actual = await task;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Same(responseMessage, actual);\n        tracer.Verify(x => x.SendAsync(request, It.IsAny<Action<string>>(), It.IsAny<Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>>(), It.IsAny<CancellationToken>()),\n            Times.Once);\n        repo.Verify(x => x.Add<string>(It.IsAny<string>(), It.IsAny<string>()),\n            Times.Never);\n    }\n\n    [Fact]\n    public void AddTraceId()\n    {\n        // Arrange\n        var addTraceId = handler.GetType().GetMethod(nameof(AddTraceId), BindingFlags.Instance | BindingFlags.NonPublic);\n        Tuple<string, string> added = null;\n        repo.Setup(x => x.Add<string>(It.IsAny<string>(), It.IsAny<string>()))\n            .Callback<string, string>((k, v) => added = new(k, v))\n            .Returns(new OkResponse());\n\n        // Act\n        addTraceId.Invoke(handler, [TestID]);\n\n        // Assert\n        repo.Verify(x => x.Add<string>(It.IsAny<string>(), It.IsAny<string>()),\n            Times.Once);\n        Assert.NotNull(added);\n        Assert.Equal(\"TraceId\", added.Item1);\n        Assert.Equal(TestID, added.Item2);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/OcelotLoggerFactoryTests.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class OcelotLoggerFactoryTests\n{\n    private readonly Mock<ILoggerFactory> _loggerFactoryMock;\n    private readonly Mock<IRequestScopedDataRepository> _scopedDataRepositoryMock;\n    private readonly OcelotLoggerFactory _factory;\n\n    public OcelotLoggerFactoryTests()\n    {\n        _loggerFactoryMock = new Mock<ILoggerFactory>();\n        _scopedDataRepositoryMock = new Mock<IRequestScopedDataRepository>();\n\n        _factory = new OcelotLoggerFactory(\n            _loggerFactoryMock.Object,\n            _scopedDataRepositoryMock.Object);\n    }\n\n    [Fact]\n    public void Constructor_GivenNullLoggerFactory_ArgumentNullExceptionThrown()\n    {\n        // Arrange\n        ILoggerFactory loggerFactory = null;\n\n        // Act + Assert\n        var exception = Assert.Throws<ArgumentNullException>(() =>\n            new OcelotLoggerFactory(loggerFactory, _scopedDataRepositoryMock.Object));\n\n        // Optional: check the parameter name\n        Assert.Equal(nameof(loggerFactory), exception.ParamName);\n    }\n\n    [Fact]\n    public void Constructor_GivenNullScopedDataRepository_ArgumentNullExceptionThrown()\n    {\n        // Arrange\n        IRequestScopedDataRepository scopedDataRepository = null;\n\n        // Act + Assert\n        var exception = Assert.Throws<ArgumentNullException>(() =>\n            new OcelotLoggerFactory(_loggerFactoryMock.Object, scopedDataRepository!));\n\n        // Optional: check the parameter name\n        Assert.Equal(nameof(scopedDataRepository), exception.ParamName);\n    }\n\n    [Fact]\n    public void CreateLogger_GivenGenericType_LoggerCreated()\n    {\n        // Arrange\n        var logger = new Mock<ILogger>();\n        _loggerFactoryMock\n            .Setup(x => x.CreateLogger(It.IsAny<string>()))\n            .Returns(logger.Object);\n\n        // Act\n        var result = _factory.CreateLogger<OcelotLoggerFactoryTests>();\n\n        // Assert\n        Assert.NotNull(result);\n        _loggerFactoryMock.Verify(\n            x => x.CreateLogger(typeof(OcelotLoggerFactoryTests).FullName),\n            Times.Once);\n    }\n\n    [Fact]\n    public void CreateLogger_GivenLoggerFactoryThrows_ExceptionThrown()\n    {\n        // Arrange\n        _loggerFactoryMock\n            .Setup(x => x.CreateLogger(It.IsAny<string>()))\n            .Throws(new InvalidOperationException(\"logger creation failed\"));\n\n        // Act\n        var exception = Record.Exception(() =>\n            _factory.CreateLogger<OcelotLoggerFactoryTests>());\n\n        // Assert\n        Assert.NotNull(exception);\n        Assert.IsType<InvalidOperationException>(exception);\n    }\n\n    [Fact]\n    public void CreateLogger_GivenFactoryDisposed_ObjectDisposedExceptionThrown()\n    {\n        // Arrange\n        _factory.Dispose();\n\n        // Act + Assert\n        Assert.Throws<ObjectDisposedException>(() =>\n            _factory.CreateLogger<OcelotLoggerFactoryTests>());\n    }\n\n    [Fact]\n    public void CreateLogger_GivenGenericType_OcelotLoggerReturned()\n    {\n        // Arrange\n        var logger = new Mock<ILogger>();\n        _loggerFactoryMock\n            .Setup(x => x.CreateLogger(It.IsAny<string>()))\n            .Returns(logger.Object);\n\n        // Act\n        var result = _factory.CreateLogger<OcelotLoggerFactoryTests>();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<OcelotLogger>(result);\n    }\n\n    [Fact]\n    public void Dispose_GivenFactoryNotDisposed_InnerFactoryDisposed()\n    {\n        // Arrange\n\n        // Act\n        _factory.Dispose();\n\n        // Assert\n        _loggerFactoryMock.Verify(x => x.Dispose(), Times.Once);\n    }\n\n    [Fact]\n    public void Dispose_GivenFactoryAlreadyDisposed_NoExceptionThrown()\n    {\n        // Arrange\n\n        // Act\n        var exception = Record.Exception(() =>\n        {\n            _factory.Dispose();\n            _factory.Dispose();\n        });\n\n        // Assert\n        Assert.Null(exception);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs",
    "content": "using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class OcelotLoggerTests\n{\n    private static readonly string _a = \"Tom\";\n    private static readonly string _b = \"Laura\";\n    private static readonly Exception _ex = new(\"oh no\");\n    private static readonly string NL = Environment.NewLine;\n\n    private OcelotLogger _logger;\n    private readonly Mock<ILogger<object>> logger = new();\n    private readonly Mock<IRequestScopedDataRepository> scopedDataRepository = new();\n    public OcelotLoggerTests()\n    {\n        logger.Setup(x => x.IsEnabled(It.IsAny<LogLevel>())).Returns(true);\n        scopedDataRepository.Setup(x => x.Get<string>(It.IsAny<string>()))\n            .Returns(new OkResponse<string>(\"1\"));\n        _logger = new OcelotLogger(logger.Object, scopedDataRepository.Object);\n    }\n\n    [Fact]\n    public void Ctor_NullChecks()\n    {\n        // Arrange, Act, Assert: argument 1\n        var ex = Assert.Throws<ArgumentNullException>(\n            () => _logger = new(null, scopedDataRepository.Object));\n        Assert.Equal(nameof(logger), ex.ParamName);\n\n        // Arrange, Act, Assert: argument 2\n        ex = Assert.Throws<ArgumentNullException>(\n            () => _logger = new(logger.Object, null));\n        Assert.Equal(nameof(scopedDataRepository), ex.ParamName);\n    }\n\n    [Fact]\n    public void GetOcelotRequestId()\n    {\n        // Arrange, Act, Assert\n        scopedDataRepository.Setup(x => x.Get<string>(It.IsAny<string>()))\n            .Returns(new OkResponse<string>(\"X\"));\n        _logger.LogTrace($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: X, PreviousRequestId: X{NL}a message from Tom to Laura\",\n            LogLevel.Trace, Times.Once());\n\n        scopedDataRepository.Setup(x => x.Get<string>(It.IsAny<string>()))\n            .Returns(new ErrorResponse<string>(new CannotFindDataError(\"error\")));\n        _logger.LogTrace($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: -, PreviousRequestId: -{NL}a message from Tom to Laura\",\n            LogLevel.Trace, Times.Once());\n    }\n\n    [Fact]\n    public void Should_log_trace()\n    {\n        // Arrange, Act, Assert\n        _logger.LogTrace(() => $\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Trace, Times.Once());\n\n        _logger.LogTrace($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Trace, Times.Exactly(2));\n    }\n\n    [Fact]\n    public void Should_log_debug()\n    {\n        // Arrange, Act, Assert\n        _logger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Debug, Times.Once());\n\n        _logger.LogDebug($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Debug, Times.Exactly(2));\n    }\n\n    [Fact]\n    public void Should_log_info()\n    {\n        // Arrange, Act, Assert\n        _logger.LogInformation(() => $\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Information, Times.Once());\n\n        _logger.LogInformation($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Information, Times.Exactly(2));\n    }\n\n    [Fact]\n    public void Should_log_warning()\n    {\n        // Arrange, Act, Assert\n        _logger.LogWarning(() => $\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Warning, Times.Once());\n\n        _logger.LogWarning($\"a message from {_a} to {_b}\");\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Warning, Times.Exactly(2));\n    }\n\n    [Fact]\n    public void Should_log_error()\n    {\n        // Arrange, Act, Assert\n        _logger.LogError(() => $\"a message from {_a} to {_b}\", _ex);\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Error, Times.Once(), _ex);\n\n        _logger.LogError($\"a message from {_a} to {_b}\", _ex);\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Error, Times.Exactly(2), _ex);\n    }\n\n    [Fact]\n    public void Should_log_critical()\n    {\n        // Arrange, Act, Assert\n        _logger.LogCritical(() => $\"a message from {_a} to {_b}\", _ex);\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Critical, Times.Once(), _ex);\n\n        _logger.LogCritical($\"a message from {_a} to {_b}\", _ex);\n        ThenLevelIsLogged($\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\",\n            LogLevel.Critical, Times.Exactly(2), _ex);\n    }\n\n    [Fact]\n    public void StaticFormatters()\n    {\n        Exception ex = new(\"test\");\n        var actual = OcelotLogger.NoFormatter(\"x\", ex);\n        Assert.Equal(\"x\", actual);\n\n        actual = OcelotLogger.ExceptionFormatter(\"y\", null);\n        Assert.Equal(\"y\", actual);\n\n        actual = OcelotLogger.ExceptionFormatter(\"z\", ex);\n        var expected = $\"z, {Environment.NewLine}Exception: System.Exception: test\";\n        Assert.Equal(expected, actual);\n    }\n\n    /// <summary>\n    /// Here mocking the original logger implementation to verify <see cref=\"ILogger.IsEnabled(LogLevel)\"/> calls.\n    /// </summary>\n    /// <param name=\"minimumLevel\">The chosen minimum log level.</param>\n    /// <returns>A mocked <see cref=\"ILogger\"/> object.</returns>\n    private static Mock<ILogger<object>> MockLogger(LogLevel? minimumLevel)\n    {\n        var logger = LoggerFactory.Create(builder =>\n            {\n                if (minimumLevel.HasValue)\n                {\n                    builder\n                        .AddSimpleConsole()\n                        .SetMinimumLevel(minimumLevel.Value);\n                }\n                else\n                {\n                    builder.AddSimpleConsole();\n                }\n            })\n            .CreateLogger<ILogger<object>>();\n\n        var mockedILogger = new Mock<ILogger<object>>();\n        mockedILogger.Setup(x => x.IsEnabled(It.IsAny<LogLevel>()))\n            .Returns(logger.IsEnabled)\n            .Verifiable();\n\n        return mockedILogger;\n    }\n\n    [Fact]\n    public void If_minimum_log_level_not_set_then_log_is_called_for_information_and_above()\n    {\n        var mockedILogger = MockLogger(null);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_none_then_log_method_is_never_called()\n    {\n        var mockedILogger = MockLogger(LogLevel.None);\n\n        var repo = new Mock<IRequestScopedDataRepository>();\n\n        var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = \"requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from Tom to Laura'\";\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_trace_then_log_is_called_for_trace_and_above()\n    {\n        var mockedILogger = MockLogger(LogLevel.Trace);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void String_func_is_never_called_when_log_level_is_disabled()\n    {\n        // Arrange\n        var mockedFunc = new Mock<Func<string>>();\n        mockedFunc.Setup(x => x.Invoke()).Returns(\"test\").Verifiable();\n        var mockedILogger = MockLogger(LogLevel.None);\n        var repo = new Mock<IRequestScopedDataRepository>();\n        var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object);\n\n        // Act\n        currentLogger.LogTrace(mockedFunc.Object);\n\n        // Assert\n        mockedFunc.Verify(x => x.Invoke(), Times.Never);\n    }\n\n    [Fact]\n    public void String_func_is_called_once_when_log_level_is_enabled()\n    {\n        // Arrange\n        var mockedFunc = new Mock<Func<string>>();\n        mockedFunc.Setup(x => x.Invoke()).Returns(\"test\").Verifiable();\n        var mockedILogger = MockLogger(LogLevel.Information);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        // Act\n        currentLogger.LogInformation(mockedFunc.Object);\n\n        // Assert\n        mockedFunc.Verify(x => x.Invoke(), Times.Once);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_debug_then_log_is_called_for_debug_and_above()\n    {\n        var mockedILogger = MockLogger(LogLevel.Debug);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_warning_then_log_is_called_for_warning_and_above()\n    {\n        var mockedILogger = MockLogger(LogLevel.Warning);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_error_then_log_is_called_for_error_and_above()\n    {\n        var mockedILogger = MockLogger(LogLevel.Error);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    [Fact]\n    public void If_minimum_log_level_set_to_critical_then_log_is_called_for_critical_and_above()\n    {\n        var mockedILogger = MockLogger(LogLevel.Critical);\n        var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object);\n\n        currentLogger.LogDebug(() => $\"a message from {_a} to {_b}\");\n        var expected = $\"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura\";\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug);\n\n        currentLogger.LogTrace(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace);\n\n        currentLogger.LogInformation(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information);\n\n        currentLogger.LogWarning(() => $\"a message from {_a} to {_b}\");\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning);\n\n        var testException = new Exception(\"test\");\n\n        currentLogger.LogError(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException);\n\n        currentLogger.LogCritical(() => $\"a message from {_a} to {_b}\", testException);\n\n        ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException);\n    }\n\n    private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Times times, Exception ex = null)\n    {\n        logger.Verify(\n            x => x.Log(expectedLogLevel, default, expected, ex, It.IsAny<Func<string, Exception, string>>()),\n            times);\n    }\n\n    private static void ThenLevelIsLogged(Mock<ILogger<object>> logger, string expected, LogLevel expectedLogLevel, Exception ex = null)\n    {\n        logger.Verify(\n            x => x.Log(\n                expectedLogLevel,\n                default,\n                expected,\n                ex,\n                It.IsAny<Func<string, Exception, string>>()), Times.Once);\n    }\n\n    private static void ThenLevelIsNotLogged(Mock<ILogger<object>> logger, string expected, LogLevel expectedLogLevel, Exception ex = null)\n    {\n        var result = logger.Object.IsEnabled(expectedLogLevel);\n\n        logger.Verify(\n            x => x.Log(\n                expectedLogLevel,\n                default,\n                expected,\n                ex,\n                It.IsAny<Func<string, Exception, string>>()), Times.Never);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/OcelotLoggerTestsForDisposal.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class OcelotLoggerTestsForDisposal\n{\n    private readonly Mock<ILogger> _innerLoggerMock;\n    private readonly Mock<IRequestScopedDataRepository> _scopedDataRepositoryMock;\n    private readonly OcelotLogger _logger;\n\n    public OcelotLoggerTestsForDisposal()\n    {\n        _innerLoggerMock = new Mock<ILogger>();\n        _innerLoggerMock\n            .Setup(x => x.IsEnabled(It.IsAny<LogLevel>()))\n            .Returns(true);\n\n        _scopedDataRepositoryMock = new Mock<IRequestScopedDataRepository>();\n        _scopedDataRepositoryMock\n            .Setup(x => x.Get<string>(It.IsAny<string>()))\n            .Returns(new OkResponse<string>(\"ID\"));\n\n        _logger = new OcelotLogger(_innerLoggerMock.Object, _scopedDataRepositoryMock.Object);\n    }\n\n    [Fact]\n    public void Dispose_GivenLoggerDisposedThenLoggingAttempt_NoUnderlyingLoggerCalled()\n    {\n        // Arrange\n        _logger.Dispose();\n\n        // Act\n        _logger.LogInformation(\"should not log\");\n\n        // Assert\n        _innerLoggerMock.Verify(x => x.Log(\n            It.IsAny<LogLevel>(),\n            It.IsAny<EventId>(),\n            It.IsAny<string>(),\n            It.IsAny<Exception>(),\n            It.IsAny<Func<string, Exception, string>>()),\n            Times.Never);\n    }\n\n    [Fact]\n    public void Dispose_CalledMultipleTimes_NoExceptionThrown()\n    {\n        // Arrange\n        _logger.Dispose();\n\n        // Act & Assert\n        Assert.Null(Record.Exception(() =>\n        {\n            _logger.Dispose();\n            _logger.Dispose();\n        }));\n    }\n\n    [Fact]\n    public void LogAfterDisposeWithFunc_GivenDisposed_NoFuncInvocation()\n    {\n        // Arrange\n        var funcMock = new Mock<Func<string>>();\n        funcMock.Setup(x => x.Invoke()).Returns(\"invoked\");\n\n        _logger.Dispose();\n\n        // Act\n        _logger.LogTrace(funcMock.Object);\n\n        // Assert\n        funcMock.Verify(x => x.Invoke(), Times.Never);\n        _innerLoggerMock.Verify(x => x.Log(\n            It.IsAny<LogLevel>(),\n            It.IsAny<EventId>(),\n            It.IsAny<string>(),\n            It.IsAny<Exception>(),\n            It.IsAny<Func<string, Exception, string>>()),\n            Times.Never);\n    }\n\n    [Fact]\n    public void Log_GivenLoggerThrowsObjectDisposedException_NoExceptionEscapes()\n    {\n        // Arrange\n        // Configure the logger to throw ObjectDisposedException when logging\n        _innerLoggerMock\n            .Setup(x => x.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()))\n            .Throws(new ObjectDisposedException($\"ILogger {nameof(_innerLoggerMock)}\"));\n\n        // Act & Assert: No exception escapes\n        var exception = Record.Exception(() =>\n            _logger.LogInformation(\"Test message\"));\n        Assert.Null(exception);\n\n        // Optional: also verify no call to underlying logger occurred (since Log threw, nothing should be invoked)\n        _innerLoggerMock.Verify(x => x.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>(), It.IsAny<Func<string, Exception, string>>()),\n            Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Logging/TracingHandlerFactoryTests.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\n\nnamespace Ocelot.UnitTests.Logging;\n\npublic class TracingHandlerFactoryTests\n{\n    private readonly TracingHandlerFactory _factory;\n    private readonly Mock<IOcelotTracer> _tracer;\n    private readonly IServiceCollection _serviceCollection;\n    private readonly IServiceProvider _serviceProvider;\n    private readonly Mock<IRequestScopedDataRepository> _repo;\n\n    public TracingHandlerFactoryTests()\n    {\n        _tracer = new Mock<IOcelotTracer>();\n        _serviceCollection = new ServiceCollection();\n        _serviceCollection.AddSingleton(_tracer.Object);\n        _serviceProvider = _serviceCollection.BuildServiceProvider(true);\n        _repo = new Mock<IRequestScopedDataRepository>();\n        _factory = new TracingHandlerFactory(_serviceProvider, _repo.Object);\n    }\n\n    [Fact]\n    public void Should_return()\n    {\n        // Arrange, Act\n        var handler = _factory.Get();\n\n        // Assert\n        Assert.IsType<OcelotHttpTracingHandler>(handler);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\r\nusing Microsoft.Extensions.Configuration.Memory;\nusing Ocelot.Middleware;\n\r\nnamespace Ocelot.UnitTests.Middleware;\r\n\r\npublic class BaseUrlFinderTests : UnitTest\r\n{\r\n    private BaseUrlFinder _baseUrlFinder;\r\n    private IConfiguration _config;\r\n    private readonly List<KeyValuePair<string, string>> _data;\r\n\r\n    public BaseUrlFinderTests()\r\n    {\r\n        _data = new List<KeyValuePair<string, string>>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_default_base_url()\r\n    {\r\n        var result = WhenIFindTheUrl();\r\n        result.ShouldBe(\"http://localhost:5000\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_memory_config_base_url()\r\n    {\r\n        GivenTheMemoryBaseUrlIs(\"http://baseurlfromconfig.com:5181\");\r\n        var result = WhenIFindTheUrl();\r\n        result.ShouldBe(\"http://baseurlfromconfig.com:5181\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_use_file_config_base_url()\r\n    {\r\n        GivenTheMemoryBaseUrlIs(\"http://localhost:7000\");\r\n        GivenTheFileBaseUrlIs(\"http://baseurlfromconfig.com:5181\");\r\n        var result = WhenIFindTheUrl();\r\n        result.ShouldBe(\"http://baseurlfromconfig.com:5181\");\r\n    }\r\n\r\n    private void GivenTheMemoryBaseUrlIs(string configValue)\r\n    {\r\n        _data.Add(new KeyValuePair<string, string>(\"BaseUrl\", configValue));\r\n    }\r\n\r\n    private void GivenTheFileBaseUrlIs(string configValue)\r\n    {\r\n        _data.Add(new KeyValuePair<string, string>(\"GlobalConfiguration:BaseUrl\", configValue));\r\n    }\r\n\r\n    private string WhenIFindTheUrl()\r\n    {\r\n        var source = new MemoryConfigurationSource\r\n        {\r\n            InitialData = _data,\r\n        };\r\n        var provider = new MemoryConfigurationProvider(source);\r\n        _config = new ConfigurationRoot(new List<IConfigurationProvider>\r\n        {\r\n            provider,\r\n        });\r\n        _baseUrlFinder = new BaseUrlFinder(_config);\r\n        return _baseUrlFinder.Find();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs",
    "content": "using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.DownstreamRouteFinder.Middleware;\nusing Ocelot.DownstreamUrlCreator;\nusing Ocelot.LoadBalancer;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.WebSockets;\n\nnamespace Ocelot.UnitTests.Middleware;\n\npublic class OcelotPipelineExtensionsTests : UnitTest\n{\n    private ApplicationBuilder _builder;\n    private RequestDelegate _handlers;\n\n    [Fact]\n    public void Should_set_up_pipeline()\n    {\n        // Arrange\n        GivenTheDepedenciesAreSetUp();\n\n        // Act\n        _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration());\n\n        // Assert\n        _handlers.ShouldNotBeNull();\n    }\n\n    [Fact]\n    public void Should_expand_pipeline()\n    {\n        // Arrange\n        GivenTheDepedenciesAreSetUp();\n        var configuration = new OcelotPipelineConfiguration();\n        configuration.MapWhenOcelotPipeline.Add((httpContext) => httpContext.WebSockets.IsWebSocketRequest, app =>\n        {\n            app.UseMiddleware<DownstreamRouteFinderMiddleware>();\n            app.UseMiddleware<DownstreamRequestInitialiserMiddleware>();\n            app.UseMiddleware<LoadBalancingMiddleware>();\n            app.UseMiddleware<DownstreamUrlCreatorMiddleware>();\n            app.UseMiddleware<WebSocketsProxyMiddleware>();\n        });\n\n        // Act\n        _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration());\n\n        // Assert\n        _handlers.ShouldNotBeNull();\n    }\n\n    private void GivenTheDepedenciesAreSetUp()\n    {\n        var root = new ConfigurationBuilder().Build();\n        var services = new ServiceCollection();\n        services.AddSingleton<IConfiguration>(root);\n        services.AddOcelot();\n        var provider = services.BuildServiceProvider(true);\n        _builder = new ApplicationBuilder(provider);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Errors.Middleware;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Middleware;\n\npublic class OcelotPiplineBuilderTests : UnitTest\n{\n    private readonly IServiceCollection _services;\n    private readonly IConfiguration _configRoot;\n    private int _counter;\n    private readonly DefaultHttpContext _httpContext;\n\n    public OcelotPiplineBuilderTests()\n    {\n        _configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());\n        _services = new ServiceCollection();\n        _services.AddSingleton(GetHostingEnvironment());\n        _services.AddSingleton(_configRoot);\n        _services.AddOcelot();\n        _httpContext = new DefaultHttpContext();\n    }\n\n    private static IWebHostEnvironment GetHostingEnvironment()\n    {\n        var environment = new Mock<IWebHostEnvironment>();\n        environment\n            .Setup(e => e.ApplicationName)\n            .Returns(typeof(OcelotPiplineBuilderTests).GetTypeInfo().Assembly.GetName().Name);\n\n        return environment.Object;\n    }\n\n    [Fact]\n    public void Should_build_generic()\n    {\n        // Arrange\n        var provider = _services.BuildServiceProvider(true);\n        IApplicationBuilder builder = new ApplicationBuilder(provider);\n        builder = builder.UseMiddleware<ExceptionHandlerMiddleware>();\n\n        // Act\n        var del = builder.Build();\n        del.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Response.StatusCode.ShouldBe(500);\n    }\n\n    [Fact]\n    public void Should_build_func()\n    {\n        // Arrange\n        _counter = 0;\n        var provider = _services.BuildServiceProvider(true);\n        IApplicationBuilder builder = new ApplicationBuilder(provider);\n        builder = builder.Use(async (ctx, next) =>\n        {\n            _counter++;\n            await next.Invoke();\n        });\n\n        // Act\n        var del = builder.Build();\n        del.Invoke(_httpContext);\n\n        // Assert\n        _counter.ShouldBe(1);\n        _httpContext.Response.StatusCode.ShouldBe(404);\n    }\n\n    [Fact]\n    public void Middleware_Multi_Parameters_Invoke()\n    {\n        // Arrange\n        var provider = _services.BuildServiceProvider(true);\n        IApplicationBuilder builder = new ApplicationBuilder(provider);\n        builder = builder.UseMiddleware<MultiParametersInvokeMiddleware>();\n\n        // Act, Assert\n        var del = builder.Build();\n        del.Invoke(_httpContext);\n    }\n\n    private class MultiParametersInvokeMiddleware : OcelotMiddleware\n    {\n#pragma warning disable IDE0060 // Remove unused parameter\n        public MultiParametersInvokeMiddleware(RequestDelegate next)\n            : base(new FakeLogger()) { }\n#pragma warning disable CA1822 // Mark members as static\n        public Task Invoke(HttpContext context, IServiceProvider serviceProvider) => Task.CompletedTask;\n#pragma warning restore CA1822 // Mark members as static\n#pragma warning restore IDE0060 // Remove unused parameter\n    }\n}\n\ninternal class FakeLogger : IOcelotLogger\n{\n    public void LogCritical(string message, Exception exception) { }\n    public void LogCritical(Func<string> messageFactory, Exception exception) { }\n    public void LogError(string message, Exception exception) { }\n    public void LogError(Func<string> messageFactory, Exception exception) { }\n    public void LogDebug(string message) { }\n    public void LogDebug(Func<string> messageFactory) { }\n    public void LogInformation(string message) { }\n    public void LogInformation(Func<string> messageFactory) { }\n    public void LogWarning(string message) { }\n    public void LogTrace(string message) { }\n    public void LogTrace(Func<string> messageFactory) { }\n    public void LogWarning(Func<string> messageFactory) { }\n    public void Dispose() { }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Multiplexer;\r\nusing static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests;\r\n\r\nnamespace Ocelot.UnitTests.Multiplexing;\r\n\r\npublic class DefinedAggregatorProviderTests : UnitTest\r\n{\r\n    private ServiceLocatorDefinedAggregatorProvider _provider;\r\n\r\n    [Fact]\r\n    public void Should_find_aggregator()\r\n    {\r\n        // Arrange\r\n        var route = new Route()\r\n        {\r\n            Aggregator = \"TestDefinedAggregator\",\r\n        };\r\n        var serviceCollection = new ServiceCollection();\r\n        serviceCollection.AddSingleton<IDefinedAggregator, TestDefinedAggregator>();\r\n        var services = serviceCollection.BuildServiceProvider(true);\r\n        _provider = new ServiceLocatorDefinedAggregatorProvider(services);\r\n\r\n        // Act\r\n        var aggregator = _provider.Get(route);\r\n\r\n        // Assert\r\n        aggregator.Data.ShouldNotBeNull();\r\n        aggregator.Data.ShouldBeOfType<TestDefinedAggregator>();\r\n        aggregator.IsError.ShouldBeFalse();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_find_aggregator()\r\n    {\r\n        // Arrange\r\n        var route = new Route()\r\n        {\r\n            Aggregator = \"TestDefinedAggregator\",\r\n        };\r\n\r\n        // Arrange: Given No Defined Aggregator\r\n        var serviceCollection = new ServiceCollection();\r\n        var services = serviceCollection.BuildServiceProvider(true);\r\n        _provider = new ServiceLocatorDefinedAggregatorProvider(services);\r\n\r\n        // Act\r\n        var aggregator = _provider.Get(route);\r\n\r\n        // Assert\r\n        aggregator.IsError.ShouldBeTrue();\r\n        aggregator.Errors[0].Message.ShouldBe(\"Could not find Aggregator: TestDefinedAggregator\");\r\n        aggregator.Errors[0].ShouldBeOfType<CouldNotFindAggregatorError>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Moq.Protected;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Multiplexer;\nusing System.Reflection;\nusing System.Security.Claims;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.Multiplexing;\n\npublic class MultiplexingMiddlewareTests : UnitTest\n{\n    private MultiplexingMiddleware _middleware;\n    private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute;\n    private int _count;\n    private readonly DefaultHttpContext _httpContext;\n    private readonly Mock<IResponseAggregatorFactory> factory;\n    private readonly Mock<IResponseAggregator> aggregator;\n    private readonly Mock<IOcelotLoggerFactory> loggerFactory;\n    private readonly Mock<IOcelotLogger> logger;\n\n    public MultiplexingMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        factory = new Mock<IResponseAggregatorFactory>();\n        aggregator = new Mock<IResponseAggregator>();\n        factory.Setup(x => x.Get(It.IsAny<Route>())).Returns(aggregator.Object);\n        loggerFactory = new Mock<IOcelotLoggerFactory>();\n        logger = new Mock<IOcelotLogger>();\n        loggerFactory.Setup(x => x.CreateLogger<MultiplexingMiddleware>()).Returns(logger.Object);\n        _middleware = new MultiplexingMiddleware(Next, loggerFactory.Object, factory.Object);\n    }\n\n    private Task Next(HttpContext context) => Task.FromResult(_count++);\n\n    [Fact]\n    public async Task Should_multiplex()\n    {\n        var route = GivenDefaultRoute(2);\n        GivenTheFollowing(route);\r\n\r\n        // Act\n        await _middleware.Invoke(_httpContext);\r\n        _count.ShouldBe(2);\n    }\n\n    [Fact]\n    public async Task Should_not_multiplex()\n    {\n        var route = new Route(new DownstreamRouteBuilder().Build());\n        GivenTheFollowing(route);\r\n\r\n        // Act\n        await _middleware.Invoke(_httpContext);\r\n        _count.ShouldBe(1);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1396\")]\n    public async Task CreateThreadContextAsync_CopyUser_ToTarget()\n    {\n        var route = new DownstreamRouteBuilder().Build();\r\n\n        // Arrange\n        GivenUser(\"test\", \"Copy\", nameof(CreateThreadContextAsync_CopyUser_ToTarget));\n\n        // Act\n        var method = _middleware.GetType().GetMethod(\"CreateThreadContextAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var actual = await (Task<HttpContext>)method.Invoke(_middleware, new object[] { _httpContext, route });\n\n        // Assert\n        AssertUsers(actual);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1396\")]\n    public async Task Invoke_ContextUser_ForwardedToDownstreamContext()\n    {\n        // Create\n        HttpContext actualContext = null;\n        _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object);\n        Task NextMe(HttpContext context)\n        {\n            actualContext = context;\n            return Next(context);\n        }\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Invoke_ContextUser_ForwardedToDownstreamContext));\n        GivenTheFollowing(GivenDefaultRoute(2));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _count.ShouldBe(2);\n        AssertUsers(actualContext);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1826\")]\n    public async Task Should_Not_Copy_Context_If_One_Downstream_Route()\n    {\n        _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object);\n        Task NextMe(HttpContext context)\n        {\n            Assert.Equal(_httpContext, context);\n            return Next(context);\n        }\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Not_Copy_Context_If_One_Downstream_Route));\n        GivenTheFollowing(GivenDefaultRoute(1));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _count.ShouldBe(1);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1826\")]\n    public async Task Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route()\n    {\n        var mock = MockMiddlewareFactory(null, null);\n\n        _middleware = mock.Object;\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route));\n        GivenTheFollowing(GivenDefaultRoute(1));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        mock.Protected().Verify<Task>(\"ProcessSingleRouteAsync\", Times.Once(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<DownstreamRoute>());\n    }\n\n    [Theory]\n    [InlineData(2)]\n    [InlineData(3)]\n    [InlineData(4)]\n    [InlineData(5)]\n    [Trait(\"PR\", \"1826\")]\n    public async Task Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route(int routesCount)\n    {\n        var mock = MockMiddlewareFactory(null, null);\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route));\n        GivenTheFollowing(GivenDefaultRoute(routesCount));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        mock.Protected().Verify<Task>(\"ProcessSingleRouteAsync\", Times.Never(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<DownstreamRoute>());\n    }\n\n    [Theory]\n    [InlineData(2)]\n    [InlineData(3)]\n    [InlineData(4)]\n    [InlineData(5)]\n    [Trait(\"PR\", \"1826\")]\n    public async Task Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once(int routesCount)\n    {\n        var mock = MockMiddlewareFactory(routesCount, null);\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once));\n        GivenTheFollowing(GivenDefaultRoute(routesCount));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        mock.Protected().Verify<Task>(\"MapAsync\", Times.Once(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<Route>(),\n            ItExpr.Is<List<HttpContext>>(list => list.Count == routesCount)\n        );\n    }\r\n\r\n    [Fact]\n    [Trait(\"PR\", \"1826\")]\n    public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route()\n    {\n        var mock = MockMiddlewareFactory(null, null);\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route));\n        GivenTheFollowing(GivenDefaultRoute(0));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        mock.Protected().Verify<Task>(\"ProcessSingleRouteAsync\", Times.Never(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<DownstreamRoute>());\n\n        mock.Protected().Verify<Task>(\"MapAsync\", Times.Never(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<Route>(),\n            ItExpr.IsAny<List<HttpContext>>());\n    }\r\n\r\n    [Theory]\n    [Trait(\"Bug\", \"2039\")]\n    [InlineData(1)] // Times.Never()\n    [InlineData(2)] // Times.Exactly(2)\n    [InlineData(3)] // Times.Exactly(3)\n    [InlineData(4)] // Times.Exactly(4)\n    public async Task Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests(int numberOfRoutes)\n    {\n        // Arrange\n        var mock = MockMiddlewareFactory(null, null);\n        GivenUser(\"test\", \"Invoke\", nameof(Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests));\n        GivenTheFollowing(GivenDefaultRoute(numberOfRoutes));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        mock.Protected().Verify<Task<Stream>>(\"CloneRequestBodyAsync\",\r\n            numberOfRoutes > 1 ? Times.Exactly(numberOfRoutes) : Times.Never(),\n            ItExpr.IsAny<HttpRequest>(),\r\n            ItExpr.IsAny<DownstreamRoute>(),\r\n            ItExpr.IsAny<CancellationToken>());\n    }\r\n\n    [Fact]\n    [Trait(\"PR\", \"1826\")]\n    public async Task If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times()\n    {\n        var mock = MockMiddlewareFactory(null, AggregateRequestDelegateFactory());\n\n        // Arrange\n        GivenUser(\"test\", \"Invoke\", nameof(If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times));\n        GivenTheFollowing(GivenRoutesWithAggregator());\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        mock.Protected().Verify<Task>(\"ProcessSingleRouteAsync\", Times.Never(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<DownstreamRoute>());\n\n        mock.Protected().Verify<Task>(\"MapAsync\", Times.Once(),\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<Route>(),\n            ItExpr.IsAny<List<HttpContext>>());\n\n        _count.ShouldBe(3);\n    }\n\n    private RequestDelegate AggregateRequestDelegateFactory()\n    {\n        return context =>\n        {\n            var responseContent = @\"[{\"\"id\"\":1,\"\"writerId\"\":1,\"\"postId\"\":2,\"\"text\"\":\"\"text1\"\"},{\"\"id\"\":2,\"\"writerId\"\":1,\"\"postId\"\":2,\"\"text\"\":\"\"text2\"\"}]\";\n            context.Items.Add(\"DownstreamResponse\", new DownstreamResponse(new StringContent(responseContent, Encoding.UTF8, \"application/json\"), HttpStatusCode.OK, new List<Header>(), \"test\"));\n\n            if (!context.Items.ContainsKey(\"TemplatePlaceholderNameAndValues\"))\n            {\n                context.Items.Add(\"TemplatePlaceholderNameAndValues\", new List<PlaceholderNameAndValue>());\n            }\n\n            _count++;\n            return Task.CompletedTask;\n        };\n    }\n\n    private Mock<MultiplexingMiddleware> MockMiddlewareFactory(int? downstreamRoutesCount, RequestDelegate requestDelegate)\n    {\n        requestDelegate ??= Next;\n\n        var mock = new Mock<MultiplexingMiddleware>(requestDelegate, loggerFactory.Object, factory.Object) { CallBase = true };\n\n        mock.Protected().Setup<Task>(\"MapAsync\",\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<Route>(),\n            downstreamRoutesCount == null ? ItExpr.IsAny<List<HttpContext>>() : ItExpr.Is<List<HttpContext>>(list => list.Count == downstreamRoutesCount)\n        ).Returns(Task.CompletedTask).Verifiable();\n\n        mock.Protected().Setup<Task>(\"ProcessSingleRouteAsync\",\n            ItExpr.IsAny<HttpContext>(),\n            ItExpr.IsAny<DownstreamRoute>()\n        ).Returns(Task.CompletedTask).Verifiable();\n\n        _middleware = mock.Object;\n        return mock;\n    }\n\n    private void GivenUser(string authentication, string name, string role)\n    {\n        var user = new ClaimsPrincipal();\n        user.AddIdentity(new(authentication, name, role));\n        _httpContext.User = user;\n    }\n\n    private void AssertUsers(HttpContext actual)\n    {\n        Assert.NotNull(actual);\n        Assert.Same(_httpContext.User, actual.User);\n        Assert.NotNull(actual.User.Identity);\n        var identity = _httpContext.User.Identity as ClaimsIdentity;\n        var actualIdentity = actual.User.Identity as ClaimsIdentity;\n        Assert.Equal(identity.AuthenticationType, actualIdentity.AuthenticationType);\n        Assert.Equal(identity.NameClaimType, actualIdentity.NameClaimType);\n        Assert.Equal(identity.RoleClaimType, actualIdentity.RoleClaimType);\n    }\n\n    private static Route GivenDefaultRoute(int count)\n    {\n        var r = new Route();\r\n        for (var i = 0; i < count; i++)\n        {\n            r.DownstreamRoute.Add(new DownstreamRouteBuilder().Build());\n        }\n\r\n        return r;\n    }\n\n    private static Route GivenRoutesWithAggregator()\n    {\n        var route1 = new DownstreamRouteBuilder().WithKey(\"Comments\").Build();\n        var route2 = new DownstreamRouteBuilder().WithKey(\"UserDetails\").Build();\n        var route3 = new DownstreamRouteBuilder().WithKey(\"PostDetails\").Build();\n\n        return new Route()\r\n        {\r\n            DownstreamRoute = [route1, route2, route3],\r\n            DownstreamRouteConfig = [\r\n                new AggregateRouteConfig { RouteKey = \"UserDetails\", JsonPath = \"$[*].writerId\", Parameter = \"userId\" },\n                new AggregateRouteConfig { RouteKey = \"PostDetails\", JsonPath = \"$[*].postId\", Parameter = \"postId\" },\n            ],\r\n            Aggregator = \"TestAggregator\",\r\n        };\n    }\n\n    private void GivenTheFollowing(Route route)\n    {\n        _downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List<PlaceholderNameAndValue>(), route);\n        _httpContext.Items.UpsertDownstreamRoute(_downstreamRoute);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs",
    "content": "using Ocelot.Configuration;\nusing Ocelot.Multiplexer;\n\r\nnamespace Ocelot.UnitTests.Multiplexing;\r\n\r\npublic class ResponseAggregatorFactoryTests : UnitTest\r\n{\r\n    private readonly InMemoryResponseAggregatorFactory _factory;\r\n    private readonly Mock<IDefinedAggregatorProvider> _provider;\r\n    private IResponseAggregator _aggregator;\r\n\r\n    public ResponseAggregatorFactoryTests()\r\n    {\r\n        _provider = new Mock<IDefinedAggregatorProvider>();\r\n        _aggregator = new SimpleJsonResponseAggregator();\r\n        _factory = new InMemoryResponseAggregatorFactory(_provider.Object, _aggregator);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_simple_json_aggregator()\r\n    {\r\n        // Arrange\r\n        var route = new Route();\r\n\r\n        // Act\r\n        _aggregator = _factory.Get(route);\r\n\r\n        // Assert\r\n        _aggregator.ShouldBeOfType<SimpleJsonResponseAggregator>();\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_user_defined_aggregator()\r\n    {\r\n        // Arrange\r\n        var route = new Route()\r\n        {\r\n            Aggregator = \"doesntmatter\",\r\n        };\r\n\r\n        // Act\r\n        _aggregator = _factory.Get(route);\r\n\r\n        // Assert\r\n        _aggregator.ShouldBeOfType<UserDefinedResponseAggregator>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs",
    "content": "﻿using Castle.Components.DictionaryAdapter;\nusing Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Middleware;\nusing Ocelot.Multiplexer;\nusing Ocelot.UnitTests.Responder;\nusing Ocelot.Values;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.Multiplexing;\n\npublic class SimpleJsonResponseAggregatorTests : UnitTest\n{\n    private readonly SimpleJsonResponseAggregator _aggregator;\n\n    public SimpleJsonResponseAggregatorTests()\n    {\n        _aggregator = new SimpleJsonResponseAggregator();\n    }\n\n    [Fact]\n    public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig()\n    {\n        var commentsDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"Comments\").Build();\n        var userDetailsDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"UserDetails\")\n            .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 0, false, \"/v1/users/{userId}\"))\n            .Build();\n        var downstreamRoutes = new List<DownstreamRoute>\n        {\n            commentsDownstreamRoute,\n            userDetailsDownstreamRoute,\n        };\n        var route = new Route()\n        {\n            DownstreamRoute = downstreamRoutes,\n            DownstreamRouteConfig = [\n                new(){RouteKey = \"UserDetails\",JsonPath = \"$[*].writerId\",Parameter = \"userId\"},\n            ],\n        };\n\n        var commentsResponseContent = @\"[{string.Emptyidstring.Empty:1,string.EmptywriterIdstring.Empty:1,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext1string.Empty},{string.Emptyidstring.Empty:2,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:2,string.Emptytextstring.Empty:string.Emptytext2string.Empty},{string.Emptyidstring.Empty:3,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext21string.Empty}]\";\n\n        var commentsDownstreamContext = new DefaultHttpContext();\n        commentsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(commentsResponseContent, Encoding.UTF8, \"application/json\"), HttpStatusCode.OK, new EditableList<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        commentsDownstreamContext.Items.UpsertDownstreamRoute(commentsDownstreamRoute);\n\n        var userDetailsResponseContent = @\"[{string.Emptyidstring.Empty:1,string.EmptyfirstNamestring.Empty:string.Emptyabolfazlstring.Empty,string.EmptylastNamestring.Empty:string.Emptyrajabpourstring.Empty},{string.Emptyidstring.Empty:2,string.EmptyfirstNamestring.Empty:string.Emptyrezastring.Empty,string.EmptylastNamestring.Empty:string.Emptyrezaeistring.Empty}]\";\n        var userDetailsDownstreamContext = new DefaultHttpContext();\n        userDetailsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(userDetailsResponseContent, Encoding.UTF8, \"application/json\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        userDetailsDownstreamContext.Items.UpsertDownstreamRoute(userDetailsDownstreamRoute);\n\n        var downstreamContexts = new List<HttpContext> { commentsDownstreamContext, userDetailsDownstreamContext };\n        var expected = \"{\\\"Comments\\\":\" + commentsResponseContent + \",\\\"UserDetails\\\":\" + userDetailsResponseContent + \"}\";\n        var upstreamContext = new DefaultHttpContext();\n\n        // Act\n        await _aggregator.Aggregate(route, upstreamContext, downstreamContexts);\n\n        // Assert\n        await ThenTheContentIs(upstreamContext, expected);\n        ThenTheContentTypeIs(upstreamContext, \"application/json\");\n        ThenTheReasonPhraseIs(upstreamContext, \"cannot return from aggregate..which reason phrase would you use?\");\n    }\n\n    [Fact]\n    public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context()\n    {\n        var billDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"Bill\").Build();\n        var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"George\").Build();\n        var downstreamRoutes = new List<DownstreamRoute>\n        {\n            billDownstreamRoute,\n            georgeDownstreamRoute,\n        };\n        var route = new Route()\n        {\n            DownstreamRoute = downstreamRoutes,\n        };\n\n        var billDownstreamContext = new DefaultHttpContext();\n        billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Bill says hi\"), HttpStatusCode.OK, new EditableList<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute);\n\n        var georgeDownstreamContext = new DefaultHttpContext();\n        georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"George says hi\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute);\n\n        var downstreamContexts = new List<HttpContext> { billDownstreamContext, georgeDownstreamContext };\n        var expected = \"{\\\"Bill\\\":Bill says hi,\\\"George\\\":George says hi}\";\n        var upstreamContext = new DefaultHttpContext();\n\n        // Act\n        await _aggregator.Aggregate(route, upstreamContext, downstreamContexts);\n\n        // Assert\n        await ThenTheContentIs(upstreamContext, expected);\n        ThenTheContentTypeIs(upstreamContext, \"application/json\");\n        ThenTheReasonPhraseIs(upstreamContext, \"cannot return from aggregate..which reason phrase would you use?\");\n    }\n\n    [Fact]\n    public async Task Should_return_error_if_any_downstreams_have_errored()\n    {\n        var billDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"Bill\").Build();\n        var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey(\"George\").Build();\n        var downstreamRoutes = new List<DownstreamRoute>\n        {\n            billDownstreamRoute,\n            georgeDownstreamRoute,\n        };\n        var route = new Route()\n        {\n            DownstreamRoute = downstreamRoutes,\n        };\n\n        var billDownstreamContext = new DefaultHttpContext();\n        billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Bill says hi\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute);\n\n        var georgeDownstreamContext = new DefaultHttpContext();\n        georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Error\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n        georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute);\n\n        georgeDownstreamContext.Items.SetError(new AnyError());\n\n        var downstreamContexts = new List<HttpContext> { billDownstreamContext, georgeDownstreamContext };\n        var expected = \"Error\";\n        var upstreamContext = new DefaultHttpContext();\n\n        // Act\n        await _aggregator.Aggregate(route, upstreamContext, downstreamContexts);\n\n        // Assert\n        await ThenTheContentIs(upstreamContext, expected);\n        ThenTheErrorIsMapped(upstreamContext, downstreamContexts);\n    }\n\n    private static void ThenTheReasonPhraseIs(DefaultHttpContext upstreamContext, string expected)\n    {\n        upstreamContext.Items.DownstreamResponse().ReasonPhrase.ShouldBe(expected);\n    }\n\n    private static void ThenTheErrorIsMapped(DefaultHttpContext upstreamContext, List<HttpContext> downstreamContexts)\n    {\n        upstreamContext.Items.Errors().ShouldBe(downstreamContexts[1].Items.Errors());\n        upstreamContext.Items.DownstreamResponse().ShouldBe(downstreamContexts[1].Items.DownstreamResponse());\n    }\n\n    private static async Task ThenTheContentIs(DefaultHttpContext upstreamContext, string expected)\n    {\n        var content = await upstreamContext.Items.DownstreamResponse().Content.ReadAsStringAsync();\n        content.ShouldBe(expected);\n    }\n\n    private static void ThenTheContentTypeIs(DefaultHttpContext upstreamContext, string expected)\n    {\n        upstreamContext.Items.DownstreamResponse().Content.Headers.ContentType.MediaType.ShouldBe(expected);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Middleware;\nusing Ocelot.Multiplexer;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\nnamespace Ocelot.UnitTests.Multiplexing;\n\npublic class UserDefinedResponseAggregatorTests : UnitTest\n{\n    private readonly UserDefinedResponseAggregator _aggregator;\n    private readonly Mock<IDefinedAggregatorProvider> _provider;\n\n    public UserDefinedResponseAggregatorTests()\n    {\n        _provider = new Mock<IDefinedAggregatorProvider>();\n        _aggregator = new UserDefinedResponseAggregator(_provider.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_aggregator()\n    {\n        // Arrange\n        var route = new Route();\n        var context = new DefaultHttpContext();\n\n        var contextA = new DefaultHttpContext();\n        contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Tom\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n\n        var contextB = new DefaultHttpContext();\n        contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Laura\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n\n        var contexts = new List<HttpContext>\n        {\n            contextA,\n            contextB,\n        };\n\n        // Arrange: Given The Provider Returns Aggregator\n        var aggregator = new TestDefinedAggregator();\n        _provider.Setup(x => x.Get(It.IsAny<Route>())).Returns(new OkResponse<IDefinedAggregator>(aggregator));\n\n        // Act\n        await _aggregator.Aggregate(route, context, contexts);\n\n        // Assert\n        _provider.Verify(x => x.Get(route), Times.Once);\n        var content = await context.Items.DownstreamResponse().Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        content.ShouldBe(\"Tom, Laura\");\n    }\n\n    [Fact]\n    public async Task Should_not_find_aggregator()\n    {\n        // Arrange\n        var route = new Route();\n        var context = new DefaultHttpContext();\n\n        var contextA = new DefaultHttpContext();\n        contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Tom\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n\n        var contextB = new DefaultHttpContext();\n        contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(\"Laura\"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\"));\n\n        var contexts = new List<HttpContext>\n        {\n            contextA,\n            contextB,\n        };\n\n        // Arrange: Given The Provider Returns Error\n        _provider.Setup(x => x.Get(It.IsAny<Route>())).Returns(new ErrorResponse<IDefinedAggregator>(new AnyError()));\n\n        // Act\n        await _aggregator.Aggregate(route, context, contexts);\n\n        // Assert\n        _provider.Verify(x => x.Get(route), Times.Once);\n        context.Items.Errors().Count.ShouldBeGreaterThan(0);\n        context.Items.Errors().Count.ShouldBe(1);\n    }\n\n    public class TestDefinedAggregator : IDefinedAggregator\n    {\n        public async Task<DownstreamResponse> Aggregate(List<HttpContext> responses)\n        {\n            var tom = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync();\n            var laura = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync();\n            var content = $\"{tom}, {laura}\";\n            var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList();\n            return new DownstreamResponse(new StringContent(content), HttpStatusCode.OK, headers, \"some reason\");\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Ocelot.UnitTests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <VersionPrefix>0.0.0-dev</VersionPrefix>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>disable</ImplicitUsings>\n    <Nullable>disable</Nullable>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <AssemblyName>Ocelot.UnitTests</AssemblyName>\n    <OutputType>Exe</OutputType>\n    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>\n    <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>\n    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>\n    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>\n    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <GenerateDocumentationFile>True</GenerateDocumentationFile>\n    <CodeAnalysisRuleSet>..\\..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n    <NoWarn>$(NoWarn);CS0618;CS1591</NoWarn>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <DebugType>full</DebugType>\n    <DebugSymbols>True</DebugSymbols>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ocelot\\Ocelot.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Consul\\Ocelot.Provider.Consul.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Eureka\\Ocelot.Provider.Eureka.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Kubernetes\\Ocelot.Provider.Kubernetes.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ocelot.Provider.Polly\\Ocelot.Provider.Polly.csproj\" />\n    <ProjectReference Include=\"..\\..\\testing\\Ocelot.Testing.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Update=\"appsettings.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"mycert.pfx\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"xunit.v3\" Version=\"3.2.2\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"8.0.0\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Consul\" Version=\"1.7.14.10\" />\n    <PackageReference Include=\"Nito.AsyncEx\" Version=\"5.1.2\" /><!--See MockWebSocket-->\n    <PackageReference Include=\"Polly\" Version=\"8.6.6\" />\n    <PackageReference Include=\"Polly.Testing\" Version=\"8.6.6\" />\n    <!-- Microsoft -->\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.FileExtensions\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Console\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Debug\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.3.0\" />\n    <PackageReference Include=\"Microsoft.Reactive.Testing\" Version=\"7.0.0-preview.1\" />\n    <PackageReference Include=\"System.IdentityModel.Tokens.Jwt\" Version=\"8.16.0\" />\n    <PackageReference Update=\"Microsoft.SourceLink.GitHub\" Version=\"10.0.201\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 8.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net8.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"8.0.25\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 9.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net9.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"9.0.14\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 10.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net10.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"10.0.5\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Polly/CircuitBreakerStrategyTests.cs",
    "content": "﻿using Ocelot.Provider.Polly;\nusing Const = Ocelot.Provider.Polly.CircuitBreakerStrategy;\n\nnamespace Ocelot.UnitTests.Polly;\n\npublic class CircuitBreakerStrategyTests\n{\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0, Const.DefaultBreakDuration)] // out of range\n    [InlineData(500, Const.DefaultBreakDuration)] // out of range\n    [InlineData(501, 501)] // in range\n    [InlineData(Const.DefaultBreakDuration, Const.DefaultBreakDuration)] // in range\n    [InlineData(86_400_000 - 1, 86_400_000 - 1)] // in range\n    [InlineData(86_400_000, Const.DefaultBreakDuration)] // out of range\n    public void BreakDuration_ShouldBeInRange(int ms, int expected)\n    {\n        // Arrange, Act\n        var actual = CircuitBreakerStrategy.BreakDuration(ms);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0, Const.DefaultMinimumThroughput)] // out of range\n    [InlineData(1, Const.DefaultMinimumThroughput)] // out of range\n    [InlineData(2, 2)] // in range\n    [InlineData(Const.DefaultMinimumThroughput, Const.DefaultMinimumThroughput)] // in range\n    public void MinimumThroughput_ShouldBeTwoOrGreater(int value, int expected)\n    {\n        // Arrange, Act\n        var actual = CircuitBreakerStrategy.MinimumThroughput(value);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    [InlineData(0.0D, Const.DefaultFailureRatio)] // out of range\n    [InlineData(0.05D, 0.05D)] // in range\n    [InlineData(Const.DefaultFailureRatio, Const.DefaultFailureRatio)] // in range\n    [InlineData(0.99D, 0.99D)] // in range\n    [InlineData(1.0, Const.DefaultFailureRatio)] // out of range\n    public void FailureRatio_ShouldBeInRange(double ratio, double expected)\n    {\n        // Arrange, Act\n        var actual = CircuitBreakerStrategy.FailureRatio(ratio);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    [InlineData(0, Const.DefaultSamplingDuration)] // out of range\n    [InlineData(500, Const.DefaultSamplingDuration)] // out of range\n    [InlineData(501, 501)] // in range\n    [InlineData(Const.DefaultSamplingDuration, Const.DefaultSamplingDuration)] // in range\n    [InlineData(86_400_000 - 1, 86_400_000 - 1)] // in range\n    [InlineData(86_400_000, Const.DefaultSamplingDuration)] // out of range\n    public void SamplingDuration_ShouldBeInRange(int ms, int expected)\n    {\n        // Arrange, Act\n        var actual = CircuitBreakerStrategy.SamplingDuration(ms);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\r\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\r\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Errors;\r\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly;\nusing Ocelot.Provider.Polly.Interfaces;\r\nusing Ocelot.QualityOfService;\r\nusing Polly;\r\n\r\nnamespace Ocelot.UnitTests.Polly;\r\n\r\npublic class OcelotBuilderExtensionsTests\r\n{\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory = new();\r\n    private readonly Mock<IHttpContextAccessor> _contextAccessor = new();\r\n\r\n    [Fact]\r\n    public void DefaultErrorMapping_CallCreateRequestTimedOutError_IsTypeOfRequestTimedOutError()\r\n    {\r\n        foreach (var kv in OcelotBuilderExtensions.DefaultErrorMapping)\r\n        {\r\n            // Arrange\r\n            var type = kv.Key;\r\n            var mappingFunc = kv.Value;\r\n            object[] args = type.IsGenericType ? [new HttpResponseMessage()] : [];\r\n            var argument = (Exception)Activator.CreateInstance(type, args);\r\n\r\n            // Act\r\n            var actual = mappingFunc.Invoke(argument);\r\n\r\n            // Assert\r\n            Assert.IsType<RequestTimedOutError>(actual);\r\n        }\r\n    }\r\n\r\n    [Fact]\r\n    public void AddPolly_NoParams_ShouldBuild()\r\n    {\r\n        // Arrange\r\n        var provider = GivenServiceProvider(\r\n            ob => ob.AddPolly(),\r\n            out var route);\r\n\r\n        // Act, Assert\r\n        var del = provider.GetService<QosDelegatingHandlerDelegate>().ShouldNotBeNull();\r\n        var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull();\r\n        handler.ShouldBeOfType<PollyResiliencePipelineDelegatingHandler>();\r\n    }\r\n\r\n    [Fact]\r\n    public void AddPolly_GenericWithoutParams_ShouldBuild()\r\n    {\r\n        // Arrange\r\n        var provider = GivenServiceProvider(\r\n            ob => ob.AddPolly<MyPollyQoSResiliencePipelineProvider>(),\r\n            out var route);\r\n\r\n        // Act, Assert\r\n        var del = provider.GetService<QosDelegatingHandlerDelegate>().ShouldNotBeNull();\r\n        var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull();\r\n        handler.ShouldBeOfType<PollyResiliencePipelineDelegatingHandler>();\r\n    }\r\n\r\n    [Fact]\r\n    public void AddPolly_WithErrorMapping_ShouldBuild()\r\n    {\r\n        // Arrange\r\n        var errorMapping = OcelotBuilderExtensions.DefaultErrorMapping;\r\n        var provider = GivenServiceProvider(\r\n            ob => ob.AddPolly<MyPollyQoSResiliencePipelineProvider>(errorMapping),\r\n            out var route);\r\n\r\n        // Act, Assert\r\n        var del = provider.GetService<QosDelegatingHandlerDelegate>().ShouldNotBeNull();\r\n        var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull();\r\n        handler.ShouldBeOfType<PollyResiliencePipelineDelegatingHandler>();\r\n    }\r\n\r\n    [Fact]\r\n    public void AddPolly_WithDelegatingHandler_ShouldBuild()\r\n    {\r\n        // Arrange\r\n        var qosDelegatingHandler = new QosDelegatingHandlerDelegate(GetQosDelegatingHandler);\r\n        var provider = GivenServiceProvider(\r\n            ob => ob.AddPolly<MyPollyQoSResiliencePipelineProvider>(qosDelegatingHandler),\r\n            out var route);\r\n\r\n        // Act, Assert\r\n        var del = provider.GetService<QosDelegatingHandlerDelegate>().ShouldNotBeNull();\r\n        var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull();\r\n        handler.ShouldBeOfType<MyQosDelegatingHandlerFor_AddPolly_WithDelegatingHandler_ShouldBuild>();\r\n    }\r\n\r\n    private static DelegatingHandler GetQosDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory)\r\n        => new MyQosDelegatingHandlerFor_AddPolly_WithDelegatingHandler_ShouldBuild();\r\n\r\n    private class MyQosDelegatingHandlerFor_AddPolly_WithDelegatingHandler_ShouldBuild : DelegatingHandler\r\n    {\r\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n            => throw new Exception(\"Hello from the fake handler!\");\r\n    }\r\n\r\n    private class MyPollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider<HttpResponseMessage>\r\n    {\r\n        public ResiliencePipeline<HttpResponseMessage> GetResiliencePipeline(DownstreamRoute route) => throw new NotImplementedException();\r\n    }\r\n\r\n    private static ServiceProvider GivenServiceProvider(Action<IOcelotBuilder> withAddPolly, out DownstreamRoute route)\r\n    {\r\n        var services = new ServiceCollection();\r\n        var options = new QoSOptions(2, 200)\r\n        {\r\n            Timeout = 100,\r\n        };\r\n        route = new DownstreamRouteBuilder()\r\n            .WithQosOptions(options)\r\n            .Build();\r\n        var configuration = new ConfigurationBuilder()\r\n            .SetBasePath(Directory.GetCurrentDirectory())\r\n            .Build();\r\n        var oBuilder = services.AddOcelot(configuration);\r\n        withAddPolly(oBuilder);\r\n        return services.BuildServiceProvider(true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly;\nusing Polly;\nusing Polly.CircuitBreaker;\nusing Polly.Registry;\nusing Polly.Testing;\nusing Polly.Timeout;\nusing _TimeoutStrategy_ = Ocelot.Provider.Polly.TimeoutStrategy;\n\nnamespace Ocelot.UnitTests.Polly;\n\npublic class PollyQoSResiliencePipelineProviderTests\n{\n    #region Constructor\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0)]\n    [InlineData(1)]\n    public void Ctor_NoLoggerParam_ShouldThrowArgumentNullException(int branch)\n    {\n        // Arrange\n        IOcelotLoggerFactory factory = null;\n        if (branch >= 0)\n            factory = null;\n        if (branch >= 1)\n            factory = Mock.Of<IOcelotLoggerFactory>();\n\n        // Act\n        var ex = Assert.Throws<ArgumentNullException>(\n            () => new PollyQoSResiliencePipelineProvider(factory, null));\n\n        // Assert\n        Assert.Equal(\"loggerFactory\", ex.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    public void Ctor_NoRegistryParam_ShouldThrowArgumentNullException()\n    {\n        // Arrange\n        var factory = new Mock<IOcelotLoggerFactory>();\n        factory.Setup(x => x.CreateLogger<PollyQoSResiliencePipelineProvider>())\n            .Returns(Mock.Of<IOcelotLogger>());\n        ResiliencePipelineRegistry<OcelotResiliencePipelineKey> noRegistry = null; // !!!\n\n        // Act\n        var ex = Assert.Throws<ArgumentNullException>(\n            () => new PollyQoSResiliencePipelineProvider(factory.Object, noRegistry));\n\n        // Assert\n        Assert.Equal(\"registry\", ex.ParamName);\n    }\n    #endregion\n\n    [Fact]\n    public void ShouldBuild()\n    {\n        // Arrange\n        var options = new QoSOptions()\n        {\n            BreakDuration = CircuitBreakerStrategy.LowBreakDuration + 1, // 0.5s, minimum required by Polly\n            MinimumThroughput = 2, // 2 is the minimum required by Polly\n            Timeout = 1000, // 10ms, minimum required by Polly\n        };\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(options)\n            .Build();\n        var provider = GivenProvider();\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n\n        // Assert\n        resiliencePipeline.ShouldNotBeNull();\n        resiliencePipeline.ShouldBeOfType<ResiliencePipeline<HttpResponseMessage>>();\n        resiliencePipeline.ShouldNotBe(ResiliencePipeline<HttpResponseMessage>.Empty);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2085\")]\n    public void ShouldNotBuild_ReturnedEmpty()\n    {\n        // Arrange\n        var options = new QoSOptions(); // empty options\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(options)\n            .Build();\n        var provider = GivenProvider();\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n\n        // Assert\n        resiliencePipeline.ShouldNotBeNull();\n        resiliencePipeline.ShouldBeOfType<ResiliencePipeline<HttpResponseMessage>>();\n        resiliencePipeline.ShouldBe(ResiliencePipeline<HttpResponseMessage>.Empty);\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"2085\")]\n    [InlineData(CircuitBreakerStrategy.LowBreakDuration - 1, CircuitBreakerStrategy.DefaultBreakDuration)] // default\n    [InlineData(CircuitBreakerStrategy.LowBreakDuration, CircuitBreakerStrategy.DefaultBreakDuration)] // default\n    [InlineData(CircuitBreakerStrategy.LowBreakDuration + 1, CircuitBreakerStrategy.LowBreakDuration + 1)] // not default, exact\n    public void ShouldBuild_WithDefaultBreakDuration(int durationOfBreak, int expectedMillisecons)\n    {\n        // Arrange\n        var options = new QoSOptions()\n        {\n            BreakDuration = durationOfBreak, // 0.5s, minimum required by Polly\n            MinimumThroughput = 2, // 2 is the minimum required by Polly\n            Timeout = 1000, // 10ms, minimum required by Polly\n        };\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(options)\n            .Build();\n        var provider = GivenProvider();\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n\n        // Assert\n        resiliencePipeline.ShouldNotBeNull();\n        resiliencePipeline.ShouldBeOfType<ResiliencePipeline<HttpResponseMessage>>();\n        resiliencePipeline.ShouldNotBe(ResiliencePipeline<HttpResponseMessage>.Empty);\n        var descriptor = resiliencePipeline.GetPipelineDescriptor();\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.Count.ShouldBe(2);\n        descriptor.Strategies[0].Options.ShouldBeOfType<CircuitBreakerStrategyOptions<HttpResponseMessage>>();\n        descriptor.Strategies[1].Options.ShouldBeOfType<TimeoutStrategyOptions>();\n        var strategyOptions = descriptor.Strategies[0].Options as CircuitBreakerStrategyOptions<HttpResponseMessage>;\n        strategyOptions.ShouldNotBeNull();\n        strategyOptions.BreakDuration.ShouldBe(TimeSpan.FromMilliseconds(expectedMillisecons));\n    }\n\n    [Fact]\n    public void Should_return_same_circuit_breaker_for_given_route()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route1 = GivenDownstreamRoute(\"/\");\n        var route2 = GivenDownstreamRoute(\"/\");\n\n        // Act\n        var resiliencePipeline1 = provider.GetResiliencePipeline(route1);\n        var resiliencePipeline2 = provider.GetResiliencePipeline(route2);\n\n        // Assert\n        resiliencePipeline1.ShouldBe(resiliencePipeline2);\n\n        // Act 2\n        var resiliencePipeline3 = provider.GetResiliencePipeline(route1);\n\n        // Assert 2\n        resiliencePipeline3.ShouldBe(resiliencePipeline1);\n        resiliencePipeline3.ShouldBe(resiliencePipeline2);\n    }\n\n    [Fact]\n    public void Should_return_different_circuit_breaker_for_two_different_routes()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route1 = GivenDownstreamRoute(\"/\");\n        var route2 = GivenDownstreamRoute(\"/test\");\n\n        // Act\n        var resiliencePipeline1 = provider.GetResiliencePipeline(route1);\n        var resiliencePipeline2 = provider.GetResiliencePipeline(route2);\n\n        // Assert\n        resiliencePipeline1.ShouldNotBe(resiliencePipeline2);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2085\")]\n    public void ShouldBuild_ContainsTwoStrategies()\n    {\n        var pollyQoSResiliencePipelineProvider = GivenProvider();\n\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route);\n        resiliencePipeline.ShouldNotBeNull();\n\n        var descriptor = resiliencePipeline.GetPipelineDescriptor();\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.Count.ShouldBe(2);\n        descriptor.Strategies[0].Options.ShouldBeOfType<CircuitBreakerStrategyOptions<HttpResponseMessage>>();\n        descriptor.Strategies[1].Options.ShouldBeOfType<TimeoutStrategyOptions>();\n    }\n\n    [Fact]\n    public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", 0); // get route with 0 exceptions allowed before breaking\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.GetPipelineDescriptor();\n\n        // Assert\n        resiliencePipeline.ShouldNotBeNull();\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.Count.ShouldBe(1);\n        descriptor.Strategies.Single().Options.ShouldBeOfType<TimeoutStrategyOptions>();\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2085\")]\n    public async Task Should_throw_after_timeout()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        const int OneSecond = 1000;\n        var route = GivenDownstreamRoute(\"/\", timeOut: OneSecond);\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.OK);\n        var cancellationTokenSource = new CancellationTokenSource();\n\n        // Assert\n        await Assert.ThrowsAsync<TimeoutRejectedException>(async () =>\n\n            // Act\n            await resiliencePipeline.ExecuteAsync(async (cancellationToken) =>\n            {\n                await Task.Delay(OneSecond + 500, cancellationToken); // add 500ms to make sure it's timed out\n                return response;\n            },\n            cancellationTokenSource.Token));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"2085\")]\n    public async Task Should_not_throw_before_timeout()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        const int OneSecond = 1000;\n        var route = GivenDownstreamRoute(\"/\", timeOut: OneSecond);\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.OK);\n        var cancellationTokenSource = new CancellationTokenSource();\n\n        // Act\n        await resiliencePipeline.ExecuteAsync(async cancellationToken =>\n        {\n            await Task.Delay(OneSecond - 100, cancellationToken); // subtract 100ms to make sure it's not timed out\n            return response;\n        }, cancellationTokenSource.Token);\n\n        // Assert\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.InternalServerError)]\n    [InlineData(HttpStatusCode.NotImplemented)]\n    [InlineData(HttpStatusCode.BadGateway)]\n    [InlineData(HttpStatusCode.ServiceUnavailable)]\n    [InlineData(HttpStatusCode.GatewayTimeout)]\n    [InlineData(HttpStatusCode.HttpVersionNotSupported)]\n    [InlineData(HttpStatusCode.VariantAlsoNegotiates)]\n    [InlineData(HttpStatusCode.InsufficientStorage)]\n    [InlineData(HttpStatusCode.LoopDetected)]\n    public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode)\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(errorCode);\n\n        // Act\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Assert\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n    }\n\n    [Fact]\n    public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.OK);\n\n        // Act, Assert\n        Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode);\n        Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode);\n        Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode);\n    }\n\n    [Fact]\n    public async Task Should_throw_and_before_delay_should_not_allow_requests()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Act, Assert\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n\n        await Task.Delay(200, TestContext.Current.CancellationToken);\n\n        // Act, Assert 2\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n    }\n\n    [Fact]\n    public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Act, Assert\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n\n        await Task.Delay(6000, TestContext.Current.CancellationToken);\n\n        // Act 2\n        var response2 = await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Assert 2\n        Assert.Equal(HttpStatusCode.InternalServerError, response2.StatusCode);\n    }\n\n    [Fact]\n    public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Act, Assert\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n\n        await Task.Delay(6000, TestContext.Current.CancellationToken);\n\n        // Act, Assert 2\n        Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode);\n\n        // Act, Assert 3\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n    }\n\n    [Fact]\n    public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\");\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n\n        // Act, Assert\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n\n        await Task.Delay(10000, TestContext.Current.CancellationToken);\n\n        // Act, Assert 2\n        var response2 = new HttpResponseMessage(HttpStatusCode.OK);\n        Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response2), TestContext.Current.CancellationToken)).StatusCode);\n\n        // Act, Assert 3\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken);\n        await Assert.ThrowsAsync<BrokenCircuitException>(async () =>\n            await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken));\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(null)]\n    [InlineData(-1)]\n    [InlineData(0)]\n    public void ConfigureTimeout_NoQosTimeout_ShouldNotApplyTimeoutStrategy(int? timeout)\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", timeOut: timeout);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Single().Options.ShouldNotBeOfType<TimeoutStrategyOptions>();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    public void ConfigureTimeout_HasInvalidTimeout_ShouldUseDefaultTimeout()\n    {\n        // Arrange\n        int? invalidTimeout = _TimeoutStrategy_.LowTimeout - 1;\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", 0, invalidTimeout);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Single().Options.ShouldBeOfType<TimeoutStrategyOptions>();\n        var actual = descriptor.Strategies.Single().Options as TimeoutStrategyOptions;\n        Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(null)]\n    [InlineData(_TimeoutStrategy_.LowTimeout - 1)]\n    public void ConfigureTimeout_ValidationIsAlwaysTrue_ShouldUseDefaultTimeout(int? invalidTimeout)\n    {\n        // Arrange\n        var provider = GivenProvider<FakeTimeoutProvider>();\n        var route = GivenDownstreamRoute(\"/\", timeOut: invalidTimeout);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Count.ShouldBe(2);\n        var strategy = descriptor.Strategies.SingleOrDefault(x => x.Options.GetType() == typeof(TimeoutStrategyOptions)).ShouldNotBeNull();\n        var actual = strategy.Options as TimeoutStrategyOptions;\n        Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(null, \"Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the timeout is disabled because the Timeout (?) is either undefined, negative, or zero.\")]\n    [InlineData(-1, \"Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the timeout is disabled because the Timeout (-1) is either undefined, negative, or zero.\")]\n    public void IsConfigurationValidForTimeout_InvalidValue_ShouldLogError(int? invalidTimeout, string expectedMessage)\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", timeOut: invalidTimeout);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Single().Options.ShouldBeOfType<CircuitBreakerStrategyOptions<HttpResponseMessage>>();\n        _logger.Verify(x => x.LogError(It.IsAny<Func<string>>(), It.IsAny<Exception>()), Times.Once());\n        var message = _funcMessage?.Invoke() ?? string.Empty;\n        message.ShouldBe(expectedMessage);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    public void IsConfigurationValidForTimeout_ValidValueButIsNotValidTimeout_ShouldLogWarning()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", timeOut: _TimeoutStrategy_.LowTimeout - 1);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Count.ShouldBe(2);\n        var strategy = descriptor.Strategies.SingleOrDefault(x => x.Options.GetType() == typeof(TimeoutStrategyOptions)).ShouldNotBeNull();\n        var actual = strategy.Options as TimeoutStrategyOptions;\n        Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds);\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var message = _funcMessage?.Invoke() ?? string.Empty;\n        message.ShouldBe(\"Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the Timeout value (9) is outside the valid range (10 to 86400000 milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value (30000) will be substituted.\");\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    public void The_ReturnedWithMessagePosition()\n    {\n        // Arrange 1\n        List<Func<string>> warnings = new();\n        static string msg1() => \"A\";\n        warnings.Add(msg1);\n\n        // Act, Assert 1\n        PollyQoSResiliencePipelineProvider.The(warnings, msg1).ShouldBe(\"the\");\n\n        // Arrange 2\n        static string msg2() => \"B\";\n        warnings.Add(msg2);\n\n        // Act, Assert 2\n        var nl = Environment.NewLine;\n        PollyQoSResiliencePipelineProvider.The(warnings, msg1).ShouldBe($\"{nl}  1. The\");\n        PollyQoSResiliencePipelineProvider.The(warnings, msg2).ShouldBe($\"{nl}  2. The\");\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    [InlineData(null, \"Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the circuit breaker is disabled because the MinimumThroughput value (?) is either undefined, negative, or zero.\")]\n    [InlineData(-1, \"Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the circuit breaker is disabled because the MinimumThroughput value (-1) is either undefined, negative, or zero.\")]\n    public void IsConfigurationValidForCircuitBreaker_InvalidValue_ShouldLogError(int? exceptionsAllowedBeforeBreaking, string expectedMessage)\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var route = GivenDownstreamRoute(\"/\", exceptionsAllowedBeforeBreaking, 555);\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Single().Options.ShouldBeOfType<TimeoutStrategyOptions>();\n        _logger.Verify(x => x.LogError(It.IsAny<Func<string>>(), It.IsAny<Exception>()), Times.Once());\n        var message = _funcMessage?.Invoke() ?? string.Empty;\n        message.ShouldBe(expectedMessage);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    public void IsConfigurationValidForCircuitBreaker_InvalidOptions_ShouldLogWarning()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var invalidOptions = new QoSOptions()\n        {\n            MinimumThroughput = 1, // invalid\n            BreakDuration = 0,\n            FailureRatio = 0.0D,\n            SamplingDuration = 0,\n            Timeout = _TimeoutStrategy_.DefTimeout, // but timeout is valid\n        };\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(invalidOptions)\n            .WithUpstreamPathTemplate(new(\"/\", 1, false, \"/\"))\n            .Build();\n\n        // Act\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Count.ShouldBe(2);\n        descriptor.Strategies.Single(x => x.Options.GetType() == typeof(CircuitBreakerStrategyOptions<HttpResponseMessage>));\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var message = _funcMessage?.Invoke() ?? string.Empty;\n        message.ShouldBe(@\"Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, \n  1. The MinimumThroughput value (1) is less than the required LowMinimumThroughput threshold (2). Therefore, increase MinimumThroughput to at least 2 or higher. Until then, the default value (100) will be substituted.\n  2. The BreakDuration value (0) is outside the valid range (500 to 86400000 milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value (5000) will be substituted.\n  3. The FailureRatio value (0) is outside the valid range (0 to 1). Therefore, ensure the ratio falls within this range; otherwise, the default value (0.1) will be substituted.\n  4. The SamplingDuration value (0) is outside the valid range (500 to 86400000 milliseconds). Therefore, ensure the duration falls within this range; otherwise, the default value (30000) will be substituted.\");\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    public void IsConfigurationValidForCircuitBreaker_NullOptions_ShouldLogWarning()\n    {\n        // Arrange\n        var provider = GivenProvider();\n        var nullOptions = new QoSOptions(1, null) // invalid\n        {\n            Timeout = _TimeoutStrategy_.DefTimeout, // but timeout is valid\n        };\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(nullOptions)\n            .WithUpstreamPathTemplate(new(\"/\", 1, false, \"/\"))\n            .Build();\n\n        // Act 2\n        var resiliencePipeline = provider.GetResiliencePipeline(route);\n        var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor();\n\n        // Assert 2\n        descriptor.ShouldNotBeNull();\n        descriptor.Strategies.ShouldNotBeEmpty();\n        descriptor.Strategies.Count.ShouldBe(2);\n        descriptor.Strategies.ShouldContain(x => x.Options.GetType() == typeof(CircuitBreakerStrategyOptions<HttpResponseMessage>));\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var message = _funcMessage?.Invoke() ?? string.Empty;\n        message.ShouldBe(\"Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the MinimumThroughput value (1) is less than the required LowMinimumThroughput threshold (2). Therefore, increase MinimumThroughput to at least 2 or higher. Until then, the default value (100) will be substituted.\");\n    }\n\n    private Func<string> _funcMessage;\n    private readonly Mock<IOcelotLogger> _logger = new();\n    private PollyQoSResiliencePipelineProvider GivenProvider() => GivenProvider<PollyQoSResiliencePipelineProvider>();\n    private PollyQoSResiliencePipelineProvider GivenProvider<T>()\n        where T : PollyQoSResiliencePipelineProvider\n    {\n        _logger.Setup(x => x.LogError(It.IsAny<Func<string>>(), It.IsAny<Exception>()))\n            .Callback<Func<string>, Exception>((f, _) => _funcMessage = f);\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>((f) => _funcMessage = f);\n        var loggerFactory = new Mock<IOcelotLoggerFactory>();\n        loggerFactory.Setup(x => x.CreateLogger<PollyQoSResiliencePipelineProvider>())\n            .Returns(_logger.Object);\n        var registry = new ResiliencePipelineRegistry<OcelotResiliencePipelineKey>();\n        return (T)Activator.CreateInstance(typeof(T), loggerFactory.Object, registry);\n    }\n\n    private static DownstreamRoute GivenDownstreamRoute(string routeTemplate, int? exceptionsAllowedBeforeBreaking = 2, int? timeOut = 10000)\n    {\n        var options = new QoSOptions(exceptionsAllowedBeforeBreaking, 5000)\n        {\n            Timeout = timeOut,\n        };\n        var upstreamPath = new UpstreamPathTemplateBuilder()\n            .WithTemplate(routeTemplate)\n            .WithContainsQueryString(false)\n            .WithPriority(1)\n            .WithOriginalValue(routeTemplate)\n            .Build();\n        return new DownstreamRouteBuilder()\n            .WithQosOptions(options)\n            .WithUpstreamPathTemplate(upstreamPath)\n            .WithLoadBalancerKey($\"{routeTemplate}|no-host|localhost:20005,localhost:20007|no-svc-ns|no-svc-name|LeastConnection|no-lb-key\")\n            .Build();\n    }\n}\n\ninternal class FakeTimeoutProvider : PollyQoSResiliencePipelineProvider\n{\n    public FakeTimeoutProvider(IOcelotLoggerFactory loggerFactory, ResiliencePipelineRegistry<OcelotResiliencePipelineKey> registry)\n        : base(loggerFactory, registry) { }\n\n    protected override bool IsConfigurationValidForTimeout(DownstreamRoute route) => true;\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Moq.Protected;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Polly;\nusing Ocelot.Provider.Polly.Interfaces;\nusing Polly;\nusing Polly.Retry;\nusing System.Reflection;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests.Polly;\n\npublic class PollyResiliencePipelineDelegatingHandlerTests\n{\n    private readonly Mock<DelegatingHandler> _innerHandler = new();\n    private readonly Mock<IOcelotLogger> _logger = new();\n    private readonly Mock<IPollyQoSResiliencePipelineProvider<HttpResponseMessage>> _pipelineProvider = new();\n    private readonly Mock<IHttpContextAccessor> _contextAccessor = new();\n    private readonly PollyResiliencePipelineDelegatingHandler _sut;\n    private Func<string> _loggerMessage;\n\n    public PollyResiliencePipelineDelegatingHandlerTests()\n    {\n        var loggerFactory = new Mock<IOcelotLoggerFactory>();\n        loggerFactory.Setup(x => x.CreateLogger<PollyResiliencePipelineDelegatingHandler>())\n            .Returns(_logger.Object);\n        _logger.Setup(x => x.LogDebug(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => _loggerMessage = f);\n        _logger.Setup(x => x.LogInformation(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => _loggerMessage = f);\n        _sut = new PollyResiliencePipelineDelegatingHandler(DownstreamRouteFactory(), _contextAccessor.Object, loggerFactory.Object);\n    }\n\n    [Fact]\n    public async Task SendAsync_WithPipeline_ExecutedByPipeline()\n    {\n        // Arrange\n        var fakeResponse = GivenHttpResponseMessage();\n        SetupInnerHandler(fakeResponse);\n        SetupResiliencePipelineProvider();\n\n        // Act\n        var actual = await InvokeAsync(\"SendAsync\");\n\n        // Assert\n        ShouldHaveTestHeaderWithoutContent(actual);\n        ShouldHaveCalledThePipelineProviderOnce();\n#if DEBUG\n        ShouldLogInformation(\"The Polly.ResiliencePipeline`1[System.Net.Http.HttpResponseMessage] pipeline has detected by QoS provider for the route with downstream URL ''. Going to execute request...\");\n#endif\n        ShouldHaveCalledTheInnerHandlerOnce();\n    }\n\n    [Fact]\n    public async Task SendAsync_NoPipeline_SentWithoutPipeline()\n    {\n        // Arrange\n        const bool PipelineIsNull = true;\n        var fakeResponse = GivenHttpResponseMessage();\n        SetupInnerHandler(fakeResponse);\n        SetupResiliencePipelineProvider(PipelineIsNull);\n\n        // Act\n        var actual = await InvokeAsync(\"SendAsync\");\n\n        // Assert\n        ShouldHaveTestHeaderWithoutContent(actual);\n        ShouldHaveCalledThePipelineProviderOnce();\n#if DEBUG\n        ShouldLogDebug(\"No pipeline was detected by QoS provider for the route with downstream URL ''.\");\n#endif\n        ShouldHaveCalledTheInnerHandlerOnce();\n    }\n\n    private void SetupInnerHandler(HttpResponseMessage fakeResponse)\n    {\n        _innerHandler.Protected()\n            .Setup<Task<HttpResponseMessage>>(\"SendAsync\", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())\n            .ReturnsAsync(fakeResponse);\n        _sut.InnerHandler = _innerHandler.Object;\n    }\n\n    private void SetupResiliencePipelineProvider(bool pipelineIsNull = false)\n    {\n        var resiliencePipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()\n            .AddRetry(new RetryStrategyOptions<HttpResponseMessage>\n            {\n                ShouldHandle = new PredicateBuilder<HttpResponseMessage>().Handle<Exception>(),\n            })\n            .Build();\n        _pipelineProvider.Setup(x => x.GetResiliencePipeline(It.IsAny<DownstreamRoute>()))\n            .Returns(pipelineIsNull ? null : resiliencePipeline);\n        var httpContext = new Mock<HttpContext>();\n        httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSResiliencePipelineProvider<HttpResponseMessage>)))\n            .Returns(_pipelineProvider.Object);\n        _contextAccessor.Setup(x => x.HttpContext)\n            .Returns(httpContext.Object);\n    }\n\n    private async Task<HttpResponseMessage> InvokeAsync(string methodName)\n    {\n        var m = typeof(PollyResiliencePipelineDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);\n        var task = (Task<HttpResponseMessage>)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None });\n        var actual = await task!;\n        return actual;\n    }\n\n    private static HttpResponseMessage GivenHttpResponseMessage([CallerMemberName] string headerValue = nameof(PollyResiliencePipelineDelegatingHandlerTests))\n    {\n        var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent);\n        fakeResponse.Headers.Add(\"X-Xunit\", headerValue);\n        return fakeResponse;\n    }\n\n    private static void ShouldHaveTestHeaderWithoutContent(HttpResponseMessage actual, [CallerMemberName] string headerValue = nameof(PollyResiliencePipelineDelegatingHandlerTests))\n    {\n        actual.ShouldNotBeNull();\n        actual.StatusCode.ShouldBe(HttpStatusCode.NoContent);\n        actual.Headers.GetValues(\"X-Xunit\").ShouldContain(headerValue);\n    }\n\n    private void ShouldHaveCalledThePipelineProviderOnce()\n    {\n        _pipelineProvider.Verify(a => a.GetResiliencePipeline(It.IsAny<DownstreamRoute>()),\n            Times.Once);\n        _pipelineProvider.VerifyNoOtherCalls();\n    }\n\n    private void ShouldHaveCalledTheInnerHandlerOnce()\n    {\n        _innerHandler.Protected().Verify<Task<HttpResponseMessage>>(\n            \"SendAsync\", Times.Once(),\n            ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());\n    }\n\n    private void ShouldLogDebug(string expected)\n    {\n        _logger.Verify(x => x.LogDebug(It.IsAny<Func<string>>()), Times.Once);\n        var msg = _loggerMessage.ShouldNotBeNull().Invoke();\n        msg.ShouldBe(expected);\n    }\n\n    private void ShouldLogInformation(string expected)\n    {\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()), Times.Once);\n        var msg = _loggerMessage.ShouldNotBeNull().Invoke();\n        msg.ShouldBe(expected);\n    }\n\n    private static DownstreamRoute DownstreamRouteFactory()\n    {\n        var options = new QoSOptions(2, 200)\n        {\n            Timeout = 100,\n        };\n        var upstreamPath = new UpstreamPathTemplateBuilder()\n            .WithTemplate(\"/\")\n            .WithContainsQueryString(false)\n            .WithPriority(1)\n            .WithOriginalValue(\"/\").Build();\n        return new DownstreamRouteBuilder()\n            .WithQosOptions(options)\n            .WithUpstreamPathTemplate(upstreamPath)\n            .Build();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Polly/TimeoutStrategyTests.cs",
    "content": "﻿using Ocelot.Provider.Polly;\nusing Const = Ocelot.Provider.Polly.TimeoutStrategy;\n\nnamespace Ocelot.UnitTests.Polly;\n\n[Collection(nameof(SequentialTests))]\npublic class TimeoutStrategyTests\n{\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0, Const.DefTimeout)] // out of range\n    [InlineData(Const.LowTimeout - 1, Const.DefTimeout)] // out of range\n    [InlineData(Const.LowTimeout, Const.DefTimeout)] // out of range\n    [InlineData(Const.LowTimeout + 1, Const.LowTimeout + 1)] // in range\n    [InlineData(Const.DefTimeout, Const.DefTimeout)] // in range\n    [InlineData(Const.HighTimeout - 1, Const.HighTimeout - 1)] // in range\n    [InlineData(Const.HighTimeout, Const.DefTimeout)] // out of range\n    [InlineData(Const.HighTimeout + 1, Const.DefTimeout)] // out of range\n    public void DefaultTimeout_Setter_ShouldBeGreaterThan10msAndLessThan24hours(int value, int expected)\n    {\n        // Arrange, Act\n        TimeoutStrategy.DefaultTimeout = value;\n\n        // Assert\n        Assert.Equal(expected, TimeoutStrategy.DefaultTimeout);\n        TimeoutStrategy.DefaultTimeout = TimeoutStrategy.DefTimeout;\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Properties/AssemblyInfo.cs",
    "content": "﻿using System.Reflection;\r\nusing System.Runtime.InteropServices;\r\n\r\n// General Information about an assembly is controlled through the following set of attributes.\r\n// Change these attribute values to modify the information associated with an assembly.\r\n[assembly: AssemblyCompany(\"Three Mammals\")]\r\n[assembly: AssemblyCopyright(\"© 2026 Three Mammals. MIT licensed OSS.\")]\r\n[assembly: AssemblyProduct(\"Ocelot Gateway\")]\r\n[assembly: AssemblyTrademark(\"Ocelot\")]\r\n\r\n// Setting ComVisible to false makes the types in this assembly not visible to COM components.\r\n// If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type.\r\n[assembly: ComVisible(false)]\r\n\r\n// The following GUID is for the ID of the typelib if this project is exposed to COM\r\n[assembly: Guid(\"54e84f1a-e525-4443-96ec-039cbd50c263\")]\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QualityOfService/FileGlobalQoSOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.QualityOfService;\n\n[Trait(\"Feat\", \"585\")]\n[Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n[Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\npublic class FileGlobalQoSOptionsTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange\n        var actual = new FileGlobalQoSOptions();\n\n        // Assert\n        AssertNullProps(actual);\n    }\n\n    [Fact]\n    public void Ctor_FileQoSOptions()\n    {\n        // Arrange\n        FileQoSOptions from = new()\n        {\n            DurationOfBreak = 1,\n            BreakDuration = 2,\n            ExceptionsAllowedBeforeBreaking = 3,\n            MinimumThroughput = 4,\n            FailureRatio = 5,\n            SamplingDuration = 6,\n            TimeoutValue = 7,\n            Timeout = 8,\n        };\n\n        // Act\n        FileGlobalQoSOptions actual = new(from);\n\n        // Assert\n        Assert.NotSame(from, actual);\n        Assert.Equivalent(from, actual);\n        Assert.Null(actual.RouteKeys);\n    }\n\n    [Fact]\n    public void Ctor_QoSOptions()\n    {\n        // Arrange\n        QoSOptions from = new(2, 3)\n        {\n            FailureRatio = 4.0D,\n            SamplingDuration = 5,\n            Timeout = 6,\n        };\n\n        // Act\n        FileGlobalQoSOptions actual = new(from);\n\n        // Assert\n        Assert.NotSame(from, actual);\n        Assert.Null(actual.RouteKeys);\n        Assert.Equal(from.BreakDuration, actual.DurationOfBreak);\n        Assert.Equal(from.BreakDuration, actual.BreakDuration);\n        Assert.Equal(from.MinimumThroughput, actual.ExceptionsAllowedBeforeBreaking);\n        Assert.Equal(from.MinimumThroughput, actual.MinimumThroughput);\n        Assert.Equal(from.FailureRatio, actual.FailureRatio);\n        Assert.Equal(from.SamplingDuration, actual.SamplingDuration);\n        Assert.Equal(from.Timeout, actual.TimeoutValue);\n        Assert.Equal(from.Timeout, actual.Timeout);\n    }\n\n    private static void AssertNullProps(FileGlobalQoSOptions actual)\n    {\n        Assert.NotNull(actual);\n        Assert.Null(actual.RouteKeys);\n\n        Assert.Null(actual.DurationOfBreak);\n        Assert.Null(actual.BreakDuration);\n        Assert.Null(actual.ExceptionsAllowedBeforeBreaking);\n        Assert.Null(actual.MinimumThroughput);\n        Assert.Null(actual.FailureRatio);\n        Assert.Null(actual.SamplingDuration);\n        Assert.Null(actual.TimeoutValue);\n        Assert.Null(actual.Timeout);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QualityOfService/FileQoSOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.QualityOfService;\n\npublic class FileQoSOptionsTests\n{\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"PR\", \"2081\")]\n    public void Ctor_Default_AllPropertiesAreNull()\n    {\n        // Arrange, Act\n        var actual = new FileQoSOptions();\n\n        // Assert\n        Assert.Null(actual.DurationOfBreak);\n        Assert.Null(actual.BreakDuration);\n        Assert.Null(actual.ExceptionsAllowedBeforeBreaking);\n        Assert.Null(actual.MinimumThroughput);\n        Assert.Null(actual.FailureRatio);\n        Assert.Null(actual.SamplingDuration);\n        Assert.Null(actual.TimeoutValue);\n        Assert.Null(actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    public void Ctor_Copying_Copied()\n    {\n        // Arrange\n        FileQoSOptions expected = new()\n        {\n            DurationOfBreak = 1,\n            BreakDuration = 2,\n            ExceptionsAllowedBeforeBreaking = 3,\n            MinimumThroughput = 4,\n            FailureRatio = 5.0D,\n            SamplingDuration = 6,\n            TimeoutValue = 7,\n            Timeout = 8,\n        };\n\n        // Act\n        FileQoSOptions actual = new(expected); // copying\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")]\n    [Trait(\"Feat\", \"2080\")]\n    public void Ctor_CopyingQoSOptions_Copied()\n    {\n        // Arrange\n        FileQoSOptions expected = new()\n        {\n            DurationOfBreak = 3,\n            BreakDuration = 3,\n            ExceptionsAllowedBeforeBreaking = 2,\n            MinimumThroughput = 2,\n            FailureRatio = 4.0D,\n            SamplingDuration = 5,\n            TimeoutValue = 6,\n            Timeout = 6,\n        };\n        QoSOptions from = new(2, 3)\n        {\n            FailureRatio = 4.0D,\n            SamplingDuration = 5,\n            Timeout = 6,\n        };\n\n        // Act\n        FileQoSOptions actual = new(from); // copying\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    private static void AssertEquality(FileQoSOptions actual, FileQoSOptions expected)\n    {\n        Assert.Equal(expected.DurationOfBreak, actual.DurationOfBreak);\n        Assert.Equal(expected.BreakDuration, actual.BreakDuration);\n        Assert.Equal(expected.ExceptionsAllowedBeforeBreaking, actual.ExceptionsAllowedBeforeBreaking);\n        Assert.Equal(expected.MinimumThroughput, actual.MinimumThroughput);\n        Assert.Equal(expected.FailureRatio, actual.FailureRatio);\n        Assert.Equal(expected.SamplingDuration, actual.SamplingDuration);\n        Assert.Equal(expected.TimeoutValue, actual.TimeoutValue);\n        Assert.Equal(expected.Timeout, actual.Timeout);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QualityOfService/QoSFactoryTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.QualityOfService;\n\nnamespace Ocelot.UnitTests.QualityOfService;\n\npublic class QoSFactoryTests\n{\n    private QoSFactory _factory;\n    private ServiceCollection _services;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IHttpContextAccessor> _contextAccessor;\n\n    public QoSFactoryTests()\n    {\n        _services = new ServiceCollection();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _contextAccessor = new Mock<IHttpContextAccessor>();\n        var provider = _services.BuildServiceProvider(true);\n        _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object);\n    }\n\n    [Fact]\n    public void Should_return_error()\n    {\n        // Arrange\n        var downstreamRoute = new DownstreamRouteBuilder().Build();\n\n        // Act\n        var handler = _factory.Get(downstreamRoute);\n\n        // Assert\n        handler.IsError.ShouldBeTrue();\n        handler.Errors[0].ShouldBeOfType<UnableToFindQoSProviderError>();\n    }\n\n    [Fact]\n    public void Should_return_handler()\n    {\n        // Arrange\n        _services = new ServiceCollection();\n\n        static DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler();\n        _services.AddSingleton<QosDelegatingHandlerDelegate>(QosDelegatingHandlerDelegate);\n        var provider = _services.BuildServiceProvider(true);\n        _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object);\n        var downstreamRoute = new DownstreamRouteBuilder().Build();\n\n        // Act\n        var handler = _factory.Get(downstreamRoute);\n\n        // Assert\n        handler.IsError.ShouldBeFalse();\n        handler.Data.ShouldBeOfType<FakeDelegatingHandler>();\n    }\n\n    private class FakeDelegatingHandler : DelegatingHandler\n    {\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QualityOfService/QoSOptionsCreatorTests.cs",
    "content": "using Microsoft.VisualStudio.TestPlatform.ObjectModel;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.QualityOfService;\n\n[Trait(\"Feat\", \"23\")] // https://github.com/ThreeMammals/Ocelot/issues/23\n[Trait(\"Release\", \"1.3.2\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2\n[Trait(\"Commit\", \"b44c025\")] // https://github.com/ThreeMammals/Ocelot/commit/b44c02510af9904a5253ab0a3e6f1f6be9cd8aeb\npublic class QoSOptionsCreatorTests : UnitTest\n{\n    private readonly QoSOptionsCreator _creator = new();\n\n    [Fact]\n    public void ShouldCreateQosOptions()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            QoSOptions = new FileQoSOptions\n            {\n                DurationOfBreak = 1,\n                ExceptionsAllowedBeforeBreaking = 2,\n                FailureRatio = 3.0D,\n                SamplingDuration = 4,\n                TimeoutValue = 5,\n            },\n        };\n        var expected = new QoSOptions(2, 1)\n        {\n            FailureRatio = 3.0D,\n            SamplingDuration = 4,\n            Timeout = 5,\n        };\n\n        // Act\n        var actual = _creator.Create(route.QoSOptions);\n\n        // Assert\n        AssertEquality(actual, expected);\n    }\n\n    #region PR 2081\n    [Fact]\n    [Trait(\"PR\", \"2081\")] // https://github.com/ThreeMammals/Ocelot/pull/2081\n    [Trait(\"Feat\", \"2080\")] // https://github.com/ThreeMammals/Ocelot/issues/2080\n    public void NoRouteOptions_ShouldCreateFromGlobalQosOptions()\n    {\n        // Arrange\n        FileGlobalConfiguration global = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 1,\n                ExceptionsAllowedBeforeBreaking = 2,\n                FailureRatio = 3.0D,\n                SamplingDuration = 4,\n                TimeoutValue = 5,\n            },\n        };\n        FileRoute route = new();\n        QoSOptions expected = new(global.QoSOptions);\n\n        // Act\n        var actual = _creator.Create(route, global);\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2081\")] // https://github.com/ThreeMammals/Ocelot/pull/2081\n    [Trait(\"Feat\", \"2080\")] // https://github.com/ThreeMammals/Ocelot/issues/2080\n    public void HasRouteOptions_ShouldCreateFromRouteQosOptions()\n    {\n        // Arrange\n        FileGlobalConfiguration global = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 1,\n                ExceptionsAllowedBeforeBreaking = 2,\n                FailureRatio = 3.0D,\n                SamplingDuration = 4,\n                TimeoutValue = 5,\n            },\n        };\n        FileRoute route = new()\n        {\n            QoSOptions = new FileQoSOptions\n            {\n                DurationOfBreak = 10,\n                ExceptionsAllowedBeforeBreaking = 20,\n                FailureRatio = 30.0D,\n                SamplingDuration = 40,\n                TimeoutValue = 50,\n            },\n        };\n        QoSOptions expected = new(route.QoSOptions);\n\n        // Act\n        var actual = _creator.Create(route, global);\n\n        // Assert\n        Assert.Equivalent(expected, actual);\n        AssertEquality(actual, expected);\n    }\n\n    private static void AssertEquality(QoSOptions actual, QoSOptions expected)\n    {\n        Assert.Equal(expected.BreakDuration, actual.BreakDuration);\n        Assert.Equal(expected.MinimumThroughput, actual.MinimumThroughput);\n        Assert.Equal(expected.FailureRatio, actual.FailureRatio);\n        Assert.Equal(expected.SamplingDuration, actual.SamplingDuration);\n        Assert.Equal(expected.Timeout, actual.Timeout);\n    }\n    #endregion PR 2081\n\n    #region PR 2339\n    [Fact]\n    [Trait(\"PR\", \"2339\")] // https://github.com/ThreeMammals/Ocelot/pull/2339\n    [Trait(\"Feat\", \"2338\")] // https://github.com/ThreeMammals/Ocelot/issues/2338\n    public void Create_FileQoSOptions()\n    {\n        // Arrange\n        FileQoSOptions options = new()\n        {\n            DurationOfBreak = 1,\n            BreakDuration = 2,\n            ExceptionsAllowedBeforeBreaking = 3,\n            MinimumThroughput = 4,\n            FailureRatio = 5,\n            SamplingDuration = 6,\n            TimeoutValue = 7,\n            Timeout = 8,\n        };\n\n        // Act\n        var actual = _creator.Create(options);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(1, actual.BreakDuration);\n        Assert.Equal(3, actual.MinimumThroughput);\n        Assert.Equal(5, actual.FailureRatio);\n        Assert.Equal(6, actual.SamplingDuration);\n        Assert.Equal(7, actual.Timeout);\n\n        // Scenario 2: Create from null\n        options = null;\n        actual = _creator.Create(options);\n        Assert.NotNull(actual);\n        Assert.Null(actual.MinimumThroughput);\n        Assert.Null(actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_FileRoute_ArgNullChecks()\n    {\n        // Arrange\n        FileRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n\n        // Act, Assert\n        var ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), ex.ParamName);\n\n        // Act, Assert\n        route = new();\n        ex = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), ex.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_FileRoute()\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 1,\n                BreakDuration = 1,\n                ExceptionsAllowedBeforeBreaking = 1,\n                MinimumThroughput = 1,\n                FailureRatio = null,\n                SamplingDuration = null,\n                TimeoutValue = 1,\n                Timeout = 1,\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 3,\n                BreakDuration = 3,\n                ExceptionsAllowedBeforeBreaking = 3,\n                MinimumThroughput = 3,\n                FailureRatio = 3,\n                SamplingDuration = 3,\n                TimeoutValue = 3,\n                Timeout = 3,\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.Equal(1, actual.BreakDuration);\n        Assert.Equal(1, actual.MinimumThroughput);\n        Assert.Equal(3, actual.FailureRatio); // global\n        Assert.Equal(3, actual.SamplingDuration); // global\n        Assert.Equal(1, actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_FileDynamicRoute_ArgNullChecks()\n    {\n        // Arrange, Act, Assert\n        FileDynamicRoute route = null;\n        FileGlobalConfiguration globalConfiguration = null;\n        var actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(route), actual.ParamName);\n\n        // Arrange, Act, Assert 2\n        route = new();\n        globalConfiguration = null;\n        actual = Assert.Throws<ArgumentNullException>(() => _creator.Create(route, globalConfiguration));\n        Assert.Equal(nameof(globalConfiguration), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_FileDynamicRoute()\n    {\n        // Arrange\n        FileDynamicRoute route = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 1,\n                BreakDuration = 1,\n                ExceptionsAllowedBeforeBreaking = 1,\n                MinimumThroughput = 1,\n                FailureRatio = null,\n                SamplingDuration = null,\n                TimeoutValue = 1,\n                Timeout = 1,\n            },\n        };\n        FileGlobalConfiguration globalConfiguration = new()\n        {\n            QoSOptions = new()\n            {\n                DurationOfBreak = 3,\n                BreakDuration = 3,\n                ExceptionsAllowedBeforeBreaking = 3,\n                MinimumThroughput = 3,\n                FailureRatio = 3,\n                SamplingDuration = 3,\n                TimeoutValue = 3,\n                Timeout = 3,\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, globalConfiguration);\n\n        // Assert\n        Assert.Equal(1, actual.BreakDuration);\n        Assert.Equal(1, actual.MinimumThroughput);\n        Assert.Equal(3, actual.FailureRatio); // global\n        Assert.Equal(3, actual.SamplingDuration); // global\n        Assert.Equal(1, actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_IRouteGrouping_NullCheck()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic);\n        IRouteGrouping grouping = null;\n        FileQoSOptions options = null;\n        FileGlobalQoSOptions globalOptions = null;\n\n        // Act\n        var wrapper = Assert.Throws<TargetInvocationException>(\n            () => method.Invoke(_creator, [grouping, options, globalOptions]));\n\n        // Assert\n        Assert.IsType<ArgumentNullException>(wrapper.InnerException);\n        var actual = (ArgumentNullException)wrapper.InnerException;\n        Assert.Equal(nameof(grouping), actual.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create() // protected\n    {\n        // Scenario 1: Null check\n        Create_IRouteGrouping_NullCheck();\n        var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic);\n        const int global = 3, route = 1;\n\n        // Scenario 2: if branches\n        FileDynamicRoute grouping = new() { Key = \"r1\" };\n        FileQoSOptions options = null;\n        FileGlobalQoSOptions globalOptions = new()\n        {\n            RouteKeys = null,\n            DurationOfBreak = global,\n            BreakDuration = global,\n            ExceptionsAllowedBeforeBreaking = global,\n            MinimumThroughput = global,\n            FailureRatio = global,\n            SamplingDuration = global,\n            TimeoutValue = global,\n            Timeout = global,\n        };\n\n        // Act, Assert : from global opts\n        var actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]);\n        Assert.Equal(global, actual.BreakDuration);\n        Assert.Equal(global, actual.MinimumThroughput);\n        Assert.Equal(global, actual.FailureRatio);\n        Assert.Equal(global, actual.SamplingDuration);\n        Assert.Equal(global, actual.Timeout);\n\n        // Arrange 2\n        options = new()\n        {\n            DurationOfBreak = route,\n            BreakDuration = route,\n            ExceptionsAllowedBeforeBreaking = route,\n            MinimumThroughput = route,\n            FailureRatio = route,\n            SamplingDuration = route,\n            TimeoutValue = route,\n            Timeout = route,\n        };\n        globalOptions.RouteKeys = [\"?\"];\n\n        // Act, Assert 2 : from route\n        actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]);\n        Assert.Equal(route, actual.BreakDuration);\n        Assert.Equal(route, actual.MinimumThroughput);\n        Assert.Equal(route, actual.FailureRatio);\n        Assert.Equal(route, actual.SamplingDuration);\n        Assert.Equal(route, actual.Timeout);\n\n        globalOptions.RouteKeys = [grouping.Key];\n        actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]);\n        Assert.Equal(route, actual.BreakDuration);\n        Assert.Equal(route, actual.MinimumThroughput);\n        Assert.Equal(route, actual.FailureRatio);\n        Assert.Equal(route, actual.SamplingDuration);\n        Assert.Equal(route, actual.Timeout);\n\n        globalOptions = null;\n        actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]);\n        Assert.Equal(route, actual.BreakDuration);\n        Assert.Equal(route, actual.MinimumThroughput);\n        Assert.Equal(route, actual.FailureRatio);\n        Assert.Equal(route, actual.SamplingDuration);\n        Assert.Equal(route, actual.Timeout);\n\n        // Arrange 3 : Merging\n        options.FailureRatio = null;\n        options.SamplingDuration = null;\n        globalOptions = new()\n        {\n            RouteKeys = null,\n            DurationOfBreak = global,\n            BreakDuration = global,\n            ExceptionsAllowedBeforeBreaking = global,\n            MinimumThroughput = global,\n            FailureRatio = global,\n            SamplingDuration = global,\n            TimeoutValue = global,\n            Timeout = global,\n        };\n        actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]);\n        Assert.Equal(route, actual.BreakDuration);\n        Assert.Equal(route, actual.MinimumThroughput);\n        Assert.Equal(global, actual.FailureRatio);\n        Assert.Equal(global, actual.SamplingDuration);\n        Assert.Equal(route, actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Create_IRouteGrouping_NoOptions()\n    {\n        // Arrange\n        var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic);\n        FileDynamicRoute route = new();\n        FileQoSOptions options = null;\n        FileGlobalQoSOptions globalOptions = null;\n\n        // Act\n        var actual = (QoSOptions)method.Invoke(_creator, [route, options, globalOptions]);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Null(actual.BreakDuration);\n        Assert.Null(actual.MinimumThroughput);\n        Assert.Null(actual.FailureRatio);\n        Assert.Null(actual.SamplingDuration);\n        Assert.Null(actual.Timeout);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2339\")]\n    [Trait(\"Feat\", \"2338\")]\n    public void Merge()\n    {\n        // Arrange null args\n        var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic);\n        FileQoSOptions options = null, global = null;\n\n        // Act\n        var actual = (QoSOptions)method.Invoke(_creator, [options, global]);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Null(actual.BreakDuration);\n        Assert.Null(actual.MinimumThroughput);\n        Assert.Null(actual.FailureRatio);\n        Assert.Null(actual.SamplingDuration);\n        Assert.Null(actual.Timeout);\n    }\n    #endregion PR 2339\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QualityOfService/QoSOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.QualityOfService;\n\npublic class QoSOptionsTests\n{\n    [Fact]\n    public void Ctor_Copy_ShouldCopy()\n    {\n        // Arrange\n        var copyee = new QoSOptions(1, 2)\n        {\n            FailureRatio = 3.0D,\n            SamplingDuration = 4,\n            Timeout = 5,\n        };\n\n        // Act\n        var actual = new QoSOptions(copyee);\n\n        // Assert\n        Assert.Equivalent(copyee, actual);\n        Assert.Equal(copyee.MinimumThroughput, actual.MinimumThroughput);\n        Assert.Equal(copyee.BreakDuration, actual.BreakDuration);\n        Assert.Equal(copyee.Timeout, actual.Timeout);\n        Assert.Equal(copyee.FailureRatio, actual.FailureRatio);\n        Assert.Equal(copyee.SamplingDuration, actual.SamplingDuration);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2073\")]\n    public void UseQos_NoOptions_ShouldNotUse()\n    {\n        // Arrange\n        var from = new FileQoSOptions();\n        var opts = new QoSOptions(from);\n\n        // Act, Assert\n        Assert.False(opts.UseQos);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(0, false)] // should not use\n    [InlineData(1, true)] // should use\n    public void UseQos_ExceptionsAllowedBeforeBreaking_ShouldUse(int exceptionsAllowed, bool expected)\n    {\n        // Arrange\n        var opts = new QoSOptions()\n        {\n            MinimumThroughput = exceptionsAllowed,\n        }; // timeoutValue is null\n\n        // Act, Assert\n        Assert.Equal(expected, opts.UseQos);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [InlineData(null, false)] // should not use\n    [InlineData(0, false)] // should not use\n    [InlineData(1, true)] // should use\n    public void UseQos_TimeoutValue_ShouldUse(int? timeout, bool expected)\n    {\n        // Arrange\n        var opts = new QoSOptions(timeout); // no exceptionsAllowedBeforeBreaking\n\n        // Act, Assert\n        Assert.Equal(expected, opts.UseQos);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Errors;\nusing Ocelot.Infrastructure.Claims;\nusing Ocelot.QueryStrings;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.QueryStrings;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-query-string-parameters\">Claims to Query String Parameters</see>.\n/// </summary>\n[Trait(\"Commit\", \"f7f4a39\")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic class AddQueriesToRequestTests : UnitTest\n{\n    private readonly AddQueriesToRequest _addQueriesToRequest;\n    private DownstreamRequest _downstreamRequest;\n    private readonly Mock<IClaimsParser> _parser;\n    private HttpRequestMessage _request;\n\n    public AddQueriesToRequestTests()\n    {\n        _request = new HttpRequestMessage(HttpMethod.Post, \"http://my.url/abc?q=123\");\n        _parser = new Mock<IClaimsParser>();\n        _addQueriesToRequest = new AddQueriesToRequest(_parser.Object);\n        _downstreamRequest = new DownstreamRequest(_request);\n    }\n\n    [Fact]\n    public void Should_add_new_queries_to_downstream_request()\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        var configuration = new List<ClaimToThing>\n        {\n            new(\"query-key\", string.Empty, string.Empty, 0),\n        };\n        var claimValue = GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        ThenTheQueryIsAdded(claimValue);\n    }\n\n    [Fact]\n    public void Should_add_new_queries_to_downstream_request_and_preserve_other_queries()\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        var configuration = new List<ClaimToThing>\n        {\n            new(\"query-key\", string.Empty, string.Empty, 0),\n        };\n        GivenTheDownstreamRequestHasQueryString(\"?test=1&test=2\");\n        var claimValue = GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        ThenTheQueryIsAdded(claimValue);\n        TheTheQueryStringIs(\"?test=1&test=2&query-key=value\");\n    }\n\n    private void TheTheQueryStringIs(string expected)\n    {\n        _downstreamRequest.Query.ShouldBe(expected);\n    }\n\n    [Fact]\n    public void Should_replace_existing_queries_on_downstream_request()\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"test\", \"data\"),\n        };\n        var configuration = new List<ClaimToThing>\n        {\n            new(\"query-key\", string.Empty, string.Empty, 0),\n        };\n        GivenTheDownstreamRequestHasQueryString(\"query-key\", \"initial\");\n        var claimValue = GivenTheClaimParserReturns(new OkResponse<string>(\"value\"));\n\n        // Act\n        var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        ThenTheQueryIsAdded(claimValue);\n    }\n\n    [Fact]\n    public void Should_return_error()\n    {\n        // Arrange\n        var claims = new List<Claim>();\n        var configuration = new List<ClaimToThing>\n        {\n            new(string.Empty, string.Empty, string.Empty, 0),\n        };\n        _ = GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error>\n        {\n            new AnyError(),\n        }));\n\n        // Act\n        var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest);\n\n        // Assert\n        result.IsError.ShouldBeTrue();\n    }\n\n    private void ThenTheQueryIsAdded(Response<string> claimValue)\n    {\n        var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString);\n        var query = queries.First(x => x.Key == \"query-key\");\n        query.Value.First().ShouldBe(claimValue.Data);\n    }\n\n    private void GivenTheDownstreamRequestHasQueryString(string queryString)\n    {\n        _request = new HttpRequestMessage(HttpMethod.Post, $\"http://my.url/abc{queryString}\");\n        _downstreamRequest = new DownstreamRequest(_request);\n    }\n\n    private void GivenTheDownstreamRequestHasQueryString(string key, string value)\n    {\n        var newUri = Microsoft.AspNetCore.WebUtilities.QueryHelpers\n            .AddQueryString(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString, key, value);\n\n        _request.RequestUri = new Uri(newUri);\n    }\n\n    private Response<string> GivenTheClaimParserReturns(Response<string> claimValue)\n    {\n        _parser.Setup(x => x.GetValue(It.IsAny<IEnumerable<Claim>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))\n            .Returns(claimValue);\n        return claimValue;\n    }\n\n    private class AnyError : Error\n    {\n        public AnyError()\n            : base(\"blahh\", OcelotErrorCode.UnknownError, 404)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.QueryStrings;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing System.Security.Claims;\n\nnamespace Ocelot.UnitTests.QueryStrings;\n\n/// <summary>\n/// Feature: <see href=\"https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/claimstransformation.rst#claims-to-query-string-parameters\">Claims to Query String Parameters</see>.\n/// </summary>\n[Trait(\"Commit\", \"f7f4a39\")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93\n[Trait(\"Release\", \"1.1.0\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0\npublic class ClaimsToQueryStringMiddlewareTests : UnitTest\n{\n    private readonly Mock<IAddQueriesToRequest> _addQueries;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ClaimsToQueryStringMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ClaimsToQueryStringMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ClaimsToQueryStringMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _addQueries = new Mock<IAddQueriesToRequest>();\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\n        _middleware = new ClaimsToQueryStringMiddleware(_next, _loggerFactory.Object, _addQueries.Object);\n    }\n\n    [Fact]\n    public async Task Should_call_add_queries_correctly()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(\"any old string\")\n            .WithClaimsToQueries(new List<ClaimToThing>\n            {\n                new(\"UserId\", \"Subject\", string.Empty, 0),\n            })\n            .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n            .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new(),\n            new Route(route, HttpMethod.Get));\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n        _addQueries.Setup(x => x.SetQueriesOnDownstreamRequest(It.IsAny<List<ClaimToThing>>(), It.IsAny<IEnumerable<Claim>>(), It.IsAny<DownstreamRequest>()))\n            .Returns(new OkResponse());\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _addQueries.Verify(x => x.SetQueriesOnDownstreamRequest(It.IsAny<List<ClaimToThing>>(), It.IsAny<IEnumerable<Claim>>(), _httpContext.Items.DownstreamRequest()),\n            Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/DistributedCacheRateLimitStorageTests.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Distributed;\nusing Newtonsoft.Json;\nusing Ocelot.RateLimiting;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class DistributedCacheRateLimitStorageTests\n{\n    private readonly Mock<IDistributedCache> _cache;\n    private readonly DistributedCacheRateLimitStorage _storage;\n\n    public DistributedCacheRateLimitStorageTests()\n    {\n        _cache = new Mock<IDistributedCache>();\n        _storage = new DistributedCacheRateLimitStorage(_cache.Object);\n    }\n\n    [Fact]\n    public void Set_ShouldSerializeAndStoreValue()\n    {\n        // Arrange\n        var id = \"test-id\";\n        var counter = new RateLimitCounter(DateTime.UtcNow, null, 5);\n        var expiration = TimeSpan.FromMinutes(1);\n        var expectedJson = JsonConvert.SerializeObject(counter);\n        var expectedBytes = Encoding.UTF8.GetBytes(expectedJson);\n        _cache.Setup(c => c.Set(id, It.IsAny<byte[]>(), It.IsAny<DistributedCacheEntryOptions>()));\n\n        // Act\n        _storage.Set(id, counter, expiration);\n\n        // Assert\n        _cache.Verify(c => c.Set(id, expectedBytes, It.Is<DistributedCacheEntryOptions>(opt => opt.AbsoluteExpirationRelativeToNow == expiration)),\n            Times.Once);\n    }\n\n    [Theory]\n    [InlineData(\"\", false)]\n    [InlineData(\"{\\\"StartedAt\\\":\\\"2025-09-11T12:00:00Z\\\",\\\"Total\\\":1}\", true)]\n    public void Exists_ShouldReturnCorrectBoolean(string storedValue, bool expected)\n    {\n        // Arrange\n        var id = \"exists-id\";\n        var bytes = Encoding.UTF8.GetBytes(storedValue);\n        _cache.Setup(c => c.Get(id)).Returns(bytes);\n\n        // Act\n        var actual = _storage.Exists(id);\n\n        // Assert\n        _cache.Verify(c => c.Get(id), Times.Once);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void Get_ShouldDeserializeStoredValue()\n    {\n        // Arrange\n        var id = \"get-id\";\n        var counter = new RateLimitCounter(DateTime.UtcNow, null, 3);\n        var json = JsonConvert.SerializeObject(counter);\n        var bytes = Encoding.UTF8.GetBytes(json);\n        _cache.Setup(c => c.Get(id)).Returns(bytes);\n\n        // Act\n        var actual = _storage.Get(id);\n\n        // Assert\n        _cache.Verify(c => c.Get(id), Times.Once);\n        Assert.NotNull(actual);\n        Assert.Equal(counter.StartedAt, actual.Value.StartedAt);\n        Assert.Equal(counter.Total, actual.Value.Total);\n    }\n\n    [Fact]\n    public void Get_ShouldReturnNull_WhenStoredValueIsNullOrEmpty()\n    {\n        // Arrange\n        var id = \"null-id\";\n        var json = string.Empty;\n        var bytes = Encoding.UTF8.GetBytes(json);\n        _cache.Setup(c => c.Get(id)).Returns(bytes);\n\n        // Act\n        var actual = _storage.Get(id);\n\n        // Assert\n        _cache.Verify(c => c.Get(id), Times.Once);\n        Assert.Null(actual);\n    }\n\n    [Fact]\n    public void Remove_ShouldCallCacheRemove()\n    {\n        // Arrange\n        var id = \"remove-id\";\n\n        // Act\n        _storage.Remove(id);\n\n        // Assert\n        _cache.Verify(c => c.Remove(id), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class FileGlobalRateLimitingTests\n{\n    [Fact]\n    public void FileGlobalRateLimit_Ctor()\n    {\n        // Arrange, Act\n        FileGlobalRateLimit actual = new();\n\n        // Assert\n        Assert.Null(actual.Name);\n        Assert.Null(actual.Pattern);\n    }\n\n    [Fact]\n    public void FileGlobalRateLimitByAspNetRule_Ctor()\n    {\n        // Arrange, Act\n        FileGlobalRateLimitByAspNetRule actual = new();\n\n        // Assert\n        Assert.Null(actual.RouteKeys);\n    }\n\n    [Fact]\n    public void FileGlobalRateLimitByHeaderRule_Ctor()\n    {\n        // Arrange, Act, Assert\n        FileGlobalRateLimitByHeaderRule actual = new();\n        Assert.Null(actual.RouteKeys);\n\n        // Arrange\n        FileRateLimitByHeaderRule from = new()\n        {\n            ClientIdHeader = \"1\",\n            ClientWhitelist = [\"2\"],\n            DisableRateLimitHeaders = true,\n            HttpStatusCode = 4,\n            QuotaExceededMessage = \"5\",\n            RateLimitCounterPrefix = \"6\",\n        };\n\n        // Act\n        FileGlobalRateLimitByHeaderRule actualG = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actualG));\n        Assert.Equivalent(from, actualG);\n    }\n\n    [Fact]\n    public void FileGlobalRateLimitByIpRule_Ctor()\n    {\n        // Arrange, Act\n        FileGlobalRateLimitByIpRule actual = new();\n\n        // Assert\n        Assert.Null(actual.RouteKeys);\n    }\n\n    [Fact]\n    public void FileGlobalRateLimitByMethodRule_Ctor()\n    {\n        // Arrange, Act\n        FileGlobalRateLimitByMethodRule actual = new();\n\n        // Assert\n        Assert.Null(actual.RouteKeys);\n    }\n\n    [Fact]\n    public void FileGlobalRateLimiting_Ctor()\n    {\n        // Arrange, Act\n        FileGlobalRateLimiting actual = new();\n\n        // Assert\n        Assert.Null(actual.ByHeader);\n        Assert.Null(actual.ByMethod);\n        Assert.Null(actual.ByIP);\n        Assert.Null(actual.ByAspNet);\n        Assert.Null(actual.Metadata);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/FileRateLimitByHeaderRuleTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class FileRateLimitByHeaderRuleTests\n{\n    [Fact]\n    public void Ctor_Parameterless()\n    {\n        // Arrange, Act\n        FileRateLimitByHeaderRule actual = new();\n\n        // Assert\n        Assert.Null(actual.ClientIdHeader);\n        Assert.Null(actual.ClientWhitelist);\n    }\n\n    [Fact]\n    public void Ctor_CopyingFrom_FileRateLimitRule()\n    {\n        // Arrange\n        FileRateLimitRule from = new()\n        {\n            EnableHeaders = false,\n            EnableRateLimiting = true,\n            KeyPrefix = \"3\",\n            Limit = 4,\n            Period = \"5\",\n            PeriodTimespan = 6D,\n            QuotaMessage = \"7\",\n            StatusCode = 8,\n            Wait = \"9\",\n        };\n\n        // Act\n        FileRateLimitByHeaderRule actual = new(from);\n        FileRateLimitRule actualRule = actual;\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actualRule);\n        Assert.Null(actual.ClientWhitelist);\n    }\n\n    [Fact]\n    public void Ctor_CopyingFrom_FileRateLimitByHeaderRule()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule from = new()\n        {\n            ClientIdHeader = \"1\",\n            ClientWhitelist = [\"2\"],\n            DisableRateLimitHeaders = true,\n            HttpStatusCode = 4,\n            QuotaExceededMessage = \"5\",\n            RateLimitCounterPrefix = \"6\",\n        };\n\n        // Act\n        FileRateLimitByHeaderRule actual = new(from);\n\n        // Assert\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n    }\n\n    [Fact]\n    public void ToString_DisabledRateLimiting_ShouldBeEmpty()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule rule = new()\n        {\n            EnableRateLimiting = false,\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Empty(actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, true, \"H+:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    [InlineData(false, true, \"H+:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    [InlineData(true, true, \"H-:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    [InlineData(null, false, \"H-:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    [InlineData(false, false, \"H+:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    [InlineData(true, false, \"H-:3:2s:w1s/HDR:hdr/WL[client1]\")]\n    public void ToString_DisableRateLimitHeaders(bool? disableRateLimitHeaders, bool enableHeadersstring, string expected)\n    {\n        // Format: <c>H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]</c>.\n        // Arrange\n        var rule = GivenRule();\n        rule.EnableHeaders = enableHeadersstring;\n        rule.DisableRateLimitHeaders = disableRateLimitHeaders;\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, \"H+:3:2s:w1s/HDR:hdr/WL-\")]\n    [InlineData(true, \"H+:3:2s:w1s/HDR:hdr/WL[]\")]\n    [InlineData(false, \"H+:3:2s:w1s/HDR:hdr/WL[cl1,cl2]\")]\n    public void ToString_ClientWhitelist(bool? isEmpty, string expected)\n    {\n        // Format: <c>H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]</c>.\n        // Arrange\n        var rule = GivenRule();\n        rule.ClientWhitelist = isEmpty switch\n        {\n            null => null,\n            true => [],\n            false => [\"cl1\", \"cl2\"],\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    private static FileRateLimitByHeaderRule GivenRule() => new()\n    {\n        Limit = 3,\n        Period = \"2s\",\n        Wait = \"1s\",\n        ClientIdHeader = \"hdr\",\n        ClientWhitelist = [\"client1\"],\n    };\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/FileRateLimitRuleTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class FileRateLimitRuleTests\n{\n    [Fact]\n    public void Ctor_Copying_ArgCheck()\n    {\n        // Arrange, Act\n        var ex = Assert.Throws<ArgumentNullException>(() => new FileRateLimitRule(null));\n\n        // Assert\n        Assert.Equal(ex.ParamName, \"from\");\n    }\n\n    [Fact]\n    public void Ctor_Copying_Copied()\n    {\n        // Arrange\n        FileRateLimitRule from = new()\n        {\n            EnableRateLimiting = true,\n            EnableHeaders = true,\n            Limit = 3,\n            Period = \"4s\",\n            PeriodTimespan = 5D,\n            Wait = \"6s\",\n            StatusCode = 7,\n            QuotaMessage = \"8\",\n            KeyPrefix = \"9\",\n        };\n\n        // Act\n        var actual = new FileRateLimitRule(from);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.False(ReferenceEquals(from, actual));\n        Assert.Equivalent(from, actual);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")] // https://github.com/ThreeMammals/Ocelot/issues/1229\n    [Trait(\"PR\", \"2294\")] // https://github.com/ThreeMammals/Ocelot/pull/2294\n    public void ToString_DisabledRateLimiting_ShouldBeEmpty()\n    {\n        // Arrange\n        FileRateLimitRule rule = new()\n        {\n            EnableRateLimiting = false,\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Empty(actual);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void ToString_HappyPath()\n    {\n        // Arrange\n        FileRateLimitRule rule = new()\n        {\n            Limit = 3,\n            Period = \"1s\",\n            Wait = \"2s\",\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Equal(\"H+:3:1s:w2s\", actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, \"H+:::w-\")]\n    [InlineData(true, \"H+:::w-\")]\n    [InlineData(false, \"H-:::w-\")]\n    public void ToString_EnableHeaders(bool? enableHeaders, string expected)\n    {\n        // Arrange\n        FileRateLimitRule rule = new()\n        {\n            EnableHeaders = enableHeaders,\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, \"H+:::w-\")]\n    [InlineData(1.234D, \"H+:::w1.234s\")]\n    public void ToString_PeriodTimespan(double? periodTimespan, string expected)\n    {\n        // Arrange\n        FileRateLimitRule rule = new()\n        {\n            PeriodTimespan = periodTimespan,\n        };\n\n        // Act\n        var actual = rule.ToString();\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/FileRateLimitingTests.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class FileRateLimitingTests\n{\n    [Fact]\n    public void FileRateLimitByAspNetRule_Ctor()\n    {\n        // Arrange, Act\n        FileRateLimitByAspNetRule actual = new();\n\n        // Assert\n        Assert.Null(actual.Policy);\n    }\n\n    [Fact]\n    public void FileRateLimitByIpRule_Ctor()\n    {\n        // Arrange, Act\n        FileRateLimitByIpRule actual = new();\n\n        // Assert\n        Assert.Null(actual.IPWhitelist);\n    }\n\n    [Fact]\n    public void FileRateLimitByMethodRule_Ctor()\n    {\n        // Arrange, Act\n        FileRateLimitByMethodRule actual = new();\n\n        // Assert\n        Assert.Null(actual.Methods);\n    }\n\n    [Fact]\n    public void FileRateLimiting_Ctor()\n    {\n        // Arrange, Act\n        FileRateLimiting actual = new();\n\n        // Assert\n        Assert.Null(actual.ByHeader);\n        Assert.Null(actual.ByMethod);\n        Assert.Null(actual.ByIP);\n        Assert.Null(actual.ByAspNet);\n        Assert.Null(actual.Metadata);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/MemoryCacheRateLimitStorageTests.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Memory;\nusing Ocelot.RateLimiting;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class MemoryCacheRateLimitStorageTests\n{\n    private readonly Mock<IMemoryCache> _cache;\n    private readonly Mock<ICacheEntry> _entry;\n    private readonly MemoryCacheRateLimitStorage _storage;\n\n    public MemoryCacheRateLimitStorageTests()\n    {\n        _cache = new Mock<IMemoryCache>();\n        _entry = new Mock<ICacheEntry>();\n        _storage = new MemoryCacheRateLimitStorage(_cache.Object);\n    }\n\n    [Fact]\n    public void Set_ShouldCreateEntryAndSetValue()\n    {\n        // Arrange\n        var id = \"test-id\";\n        var counter = new RateLimitCounter(DateTime.UtcNow, null, 1);\n        TimeSpan expiration = TimeSpan.FromMinutes(5);\n        _cache.Setup(c => c.CreateEntry(id)).Returns(_entry.Object);\n\n        // Act\n        _storage.Set(id, counter, expiration);\n\n        // Assert\n        _entry.VerifySet(e => e.Value = counter);\n        _entry.VerifySet(e => e.AbsoluteExpirationRelativeToNow = expiration);\n        _entry.Verify(e => e.Dispose());\n        _cache.Verify(c => c.CreateEntry(id), Times.Once);\n    }\n\n    [Fact]\n    public void Exists_ShouldReturnTrue_WhenKeyExists()\n    {\n        // Arrange\n        var id = \"exists-id\";\n        var counter = new RateLimitCounter(DateTime.UtcNow, null, 2);\n        object boxed = counter;\n        _cache.Setup(c => c.TryGetValue(id, out boxed))\n            .Returns(true);\n\n        // Act\n        var actual = _storage.Exists(id);\n\n        // Assert\n        Assert.True(actual);\n        _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once);\n    }\n\n    [Fact]\n    public void Exists_ShouldReturnFalse_WhenKeyDoesNotExist()\n    {\n        // Arrange\n        var id = \"missing-id\";\n        object boxed = null;\n        _cache.Setup(c => c.TryGetValue(id, out boxed))\n            .Returns(false);\n\n        // Act\n        var actual = _storage.Exists(id);\n\n        // Assert\n        Assert.False(actual);\n        _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once);\n    }\n\n    [Fact]\n    public void Get_ShouldReturnCounter_WhenKeyExists()\n    {\n        // Arrange\n        var id = \"get-id\";\n        var counter = new RateLimitCounter(DateTime.UtcNow, null, 3);\n        object boxed = counter;\n        _cache.Setup(c => c.TryGetValue(id, out boxed))\n            .Returns(true);\n\n        // Act\n        var result = _storage.Get(id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(counter.Total, result.Value.Total);\n        Assert.Equal(counter.StartedAt, result.Value.StartedAt);\n        _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once);\n    }\n\n    [Fact]\n    public void Get_ShouldReturnNull_WhenKeyDoesNotExist()\n    {\n        // Arrange\n        var id = \"null-id\";\n        object boxed = null;\n        _cache.Setup(c => c.TryGetValue(id, out boxed))\n            .Returns(false);\n\n        // Act\n        var actual = _storage.Get(id);\n\n        // Assert\n        Assert.Null(actual);\n        _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once);\n    }\n\n    [Fact]\n    public void Remove_ShouldCallCacheRemove()\n    {\n        // Arrange\n        var id = \"remove-id\";\n\n        // Act\n        _storage.Remove(id);\n\n        // Assert\n        _cache.Verify(c => c.Remove(id), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitCounterTests.cs",
    "content": "﻿using Ocelot.RateLimiting;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitCounterTests\n{\n    [Fact]\n    public void Ctor()\n    {\n        // Arrange\n        var startedAt = DateTime.Now;\n        var exceededAt = startedAt + TimeSpan.FromSeconds(1);\n\n        // Act\n        var actual = new RateLimitCounter(startedAt, exceededAt, 3);\n\n        // Assert\n        Assert.Equal(startedAt, actual.StartedAt);\n        Assert.Equal(exceededAt, actual.ExceededAt);\n        Assert.Equal(3, actual.Total);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")] // https://github.com/ThreeMammals/Ocelot/issues/1229\n    [Trait(\"PR\", \"2294\")] // https://github.com/ThreeMammals/Ocelot/pull/2294\n    public void ToString_NoExceededAt()\n    {\n        // Arrange\n        var today = new DateTime(2025, 9, 7);\n        today = today.AddHours(12);\n        today = today.AddMinutes(34);\n        today = today.AddSeconds(56.789);\n        today = today.AddMicroseconds(123.4567890);\n        RateLimitCounter counter = new(today, default, 1);\n\n        // Act\n        var actual = counter.ToString();\n\n        // Assert\n        Assert.Equal(\"1->(2025-09-07T12:34:56.7891234)\", actual);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")] // https://github.com/ThreeMammals/Ocelot/issues/1229\n    [Trait(\"PR\", \"2294\")] // https://github.com/ThreeMammals/Ocelot/pull/2294\n    public void ToString_WithExceededAt()\n    {\n        // Arrange\n        var today = new DateTime(2025, 9, 7);\n        today = today.AddHours(1);\n        today = today.AddMinutes(2);\n        today = today.AddSeconds(3);\n        today = today.AddMilliseconds(4);\n        today = today.AddMicroseconds(5);\n        TimeSpan shift = new(1, 2, 3, 4, 5, 6);\n        DateTime? exceededAt = today + shift;\n        RateLimitCounter counter = new(today, exceededAt, 2);\n\n        // Act\n        var actual = counter.ToString();\n\n        // Assert\n        Assert.Equal(\"2->(2025-09-07T01:02:03.0040050)+1.02:03:04.0050060\", actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitHeadersTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.RateLimiting;\nusing System.Collections.Generic;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitHeadersTests\n{\n    [Fact]\n    public void Ctor_Created()\n    {\n        // Arrange\n        HttpContext ctx = new DefaultHttpContext();\n        long limit = 3, remaining = 2;\n        DateTime today = DateTime.Today;\n\n        // Act\n        RateLimitHeaders actual = new(ctx, limit, remaining, today);\n\n        // Assert\n        Assert.Equal(ctx, actual.Context);\n        Assert.Equal(limit, actual.Limit);\n        Assert.Equal(remaining, actual.Remaining);\n        Assert.Equal(today, actual.Reset);\n    }\n\n    [Fact]\n    public void Ctor_Parameterless()\n    {\n        // Arrange\n        ConstructorInfo ctor = typeof(RateLimitHeaders).GetConstructor(\n                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,\n                binder: null, types: Type.EmptyTypes, modifiers: null);\n        Assert.NotNull(ctor);\n        Assert.False(ctor.IsPublic);\n\n        // Act\n        RateLimitHeaders actual = ctor.Invoke(null) as RateLimitHeaders;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(default, actual.Context);\n        Assert.Equal(default, actual.Limit);\n        Assert.Equal(default, actual.Remaining);\n        Assert.Equal(default, actual.Reset);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitOptionsCreatorTests : UnitTest\n{\n    private readonly RateLimitOptionsCreator _creator = new();\n\n    [Fact]\n    [Trait(\"PR\", \"58\")] // https://github.com/ThreeMammals/Ocelot/pull/58\n    [Trait(\"Release\", \"1.4.0\")]\n    public void Should_create_rate_limit_options()\n    {\n        // Arrange\n        var route = new FileRoute\n        {\n            RateLimitOptions = new()\n            {\n                ClientWhitelist = new List<string>(),\n                Period = \"Period\",\n                Limit = 1,\n                Wait = \"OneSecond\",\n                EnableRateLimiting = true,\n            },\n        };\n        var globalConfig = new FileGlobalConfiguration\n        {\n            RateLimitOptions = new()\n            {\n                ClientIdHeader = \"ClientIdHeader\",\n                EnableHeaders = true,\n                QuotaExceededMessage = \"QuotaMessage\",\n                RateLimitCounterPrefix = \"RateLimitCounterPrefix\",\n                HttpStatusCode = 200,\n            },\n        };\n        var options = route.RateLimitOptions;\n        RateLimitOptions expected = new()\n        {\n            ClientIdHeader = \"ClientIdHeader\",\n            ClientWhitelist = options.ClientWhitelist,\n            EnableHeaders = true,\n            EnableRateLimiting = true,\n            StatusCode = 200,\n            QuotaMessage = \"QuotaMessage\",\n            KeyPrefix = \"RateLimitCounterPrefix\",\n            Rule = new(options.Period, options.Wait, options.Limit.Value),\n        };\n        bool enabled = true;\n\n        // Act\n        var result = _creator.Create(route, globalConfig);\n\n        // Assert\n        enabled.ShouldBeTrue();\n        result.ClientIdHeader.ShouldBe(expected.ClientIdHeader);\n        result.ClientWhitelist.ShouldBe(expected.ClientWhitelist);\n        result.EnableHeaders.ShouldBe(expected.EnableHeaders);\n        result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting);\n        result.StatusCode.ShouldBe(expected.StatusCode);\n        result.QuotaMessage.ShouldBe(expected.QuotaMessage);\n        result.KeyPrefix.ShouldBe(expected.KeyPrefix);\n        result.Rule.Limit.ShouldBe(expected.Rule.Limit);\n        result.Rule.Period.ShouldBe(expected.Rule.Period);\n        result.Rule.Wait.ShouldBe(expected.Rule.Wait);\n    }\n\n    #region PR 2294\n    [Fact]\n    [Trait(\"Feat\", \"1229\")] // https://github.com/ThreeMammals/Ocelot/issues/1229\n    [Trait(\"PR\", \"2294\")] // https://github.com/ThreeMammals/Ocelot/pull/2294\n    public void Create_ArgumentNullChecks()\n    {\n        // Arrange, Act, Assert\n        Assert.Throws<ArgumentNullException>(() => _creator.Create(null, new()));\n        Assert.Throws<ArgumentNullException>(() => _creator.Create(new FileRoute(), null));\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Create_GlobalRouteKeysCollectionIsNull_IsGlobalDefaultsToTrue()\n    {\n        // Arrange, Act, Assert: branch 1\n        FileRoute route = new();\n        FileGlobalConfiguration global = new();\n        var actual = _creator.Create(route, global);\n        Assert.NotNull(actual);\n        Assert.False(actual.EnableRateLimiting);\n\n        // Arrange, Act, Assert: branch 2\n        global.RateLimitOptions = new(); // -> RouteKeys is null\n        actual = _creator.Create(route, global);\n        Assert.NotNull(actual);\n        Assert.True(actual.EnableRateLimiting);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Create_GlobalRouteKeys_ContainsRouteKey(bool contains)\n    {\n        // Arrange\n        FileRoute route = new() { Key = contains ? \"R1\" : \"?\" };\n        FileGlobalConfiguration global = new()\n        {\n            RateLimitOptions = new()\n            {\n                RouteKeys = [\"R1\"],\n                EnableRateLimiting = true,\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(route, global);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(contains, actual.EnableRateLimiting);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null, null, true)]\n    [InlineData(false, null, false)]\n    [InlineData(null, false, false)]\n    [InlineData(false, false, false)]\n    public void Create_DisabledRateLimiting(bool? ruleEnableRateLimiting, bool? globalEnableRateLimiting, bool expected)\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            RateLimitOptions = new() { EnableRateLimiting = ruleEnableRateLimiting },\n        };\n        FileGlobalConfiguration global = new()\n        {\n            RateLimitOptions = new() { EnableRateLimiting = globalEnableRateLimiting },\n        };\n\n        // Act\n        var actual = _creator.Create(route, global);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual.EnableRateLimiting);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(true, null, false, \"rule\")]\n    [InlineData(null, true, true, \"globalRule\")]\n    [InlineData(true, true, false, \"rule\")]\n    [InlineData(true, true, true, \"rule\")]\n    [InlineData(null, null, true, \"Oc-Client\")]\n    public void Create_ByHeaderRules(bool? hasRule, bool? hasGlobal, bool isGlobal, string expected)\n    {\n        // Arrange\n        FileRoute route = new()\n        {\n            Key = \"R1\",\n            RateLimitOptions = !hasRule.HasValue ? null : new() { ClientIdHeader = \"rule\" },\n        };\n        FileGlobalConfiguration global = new()\n        {\n            RateLimitOptions = !hasGlobal.HasValue ? null :\n                new() { RouteKeys = [isGlobal ? \"R1\" : \"?\"], ClientIdHeader = \"globalRule\" },\n        };\n\n        // Act\n        var actual = _creator.Create(route, global);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual.ClientIdHeader);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void MergeHeaderRules_ArgumentNullChecks()\n    {\n        // Arrange\n        MethodInfo method = _creator.GetType().GetMethod(\"MergeHeaderRules\", BindingFlags.Instance | BindingFlags.NonPublic);\n        FileRateLimitByHeaderRule rule = new();\n        FileGlobalRateLimitByHeaderRule globalRule = new();\n\n        // Act\n        var ex1 = Assert.Throws<TargetInvocationException>(() => method?.Invoke(_creator, [null, globalRule]));\n        var ex2 = Assert.Throws<TargetInvocationException>(() => method?.Invoke(_creator, [rule, null]));\n\n        // Assert\n        Assert.NotNull(ex1.InnerException);\n        Assert.True(ex1.InnerException is ArgumentNullException);\n        Assert.Equal(nameof(rule), ((ArgumentNullException)ex1.InnerException).ParamName);\n        Assert.NotNull(ex2.InnerException);\n        Assert.True(ex2.InnerException is ArgumentNullException);\n        Assert.Equal(nameof(globalRule), ((ArgumentNullException)ex2.InnerException).ParamName);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void MergeHeaderRules_FromGlobal()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule rule = new();\n        FileGlobalRateLimitByHeaderRule global = new()\n        {\n            ClientIdHeader = \"111\",\n            ClientWhitelist = [\"222\"],\n            DisableRateLimitHeaders = null,\n            EnableHeaders = false,\n            EnableRateLimiting = true,\n            HttpStatusCode = 300,\n            StatusCode = 400,\n            QuotaExceededMessage = \"55\",\n            QuotaMessage = \"66\",\n            RateLimitCounterPrefix = \"77\",\n            KeyPrefix = \"88\",\n            Period = \"9s\",\n            PeriodTimespan = 10.0D,\n            Wait = \"11s\",\n            Limit = 12,\n        };\n        MethodInfo method = _creator.GetType().GetMethod(\"MergeHeaderRules\", BindingFlags.Instance | BindingFlags.NonPublic);\n\n        // Act\n        var actual = method?.Invoke(_creator, [rule, global]) as RateLimitOptions;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(\"111\", actual.ClientIdHeader);\n        Assert.Contains(\"222\", actual.ClientWhitelist);\n        Assert.False(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(300, actual.StatusCode);\n        Assert.Equal(\"55\", actual.QuotaMessage);\n        Assert.Equal(\"77\", actual.KeyPrefix);\n        Assert.Equal(\"12/9s/w10s\", actual.Rule.ToString());\n    }\n    #endregion\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void Create_FileGlobalConfiguration(bool hasOptions)\n    {\n        // Arrange\n        FileGlobalConfiguration global = new()\n        {\n            RateLimitOptions = !hasOptions ? null : new()\n            {\n                ClientIdHeader = \"globalRule\",\n            },\n        };\n\n        // Act\n        var actual = _creator.Create(global);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(hasOptions ? \"globalRule\" : \"Oc-Client\", actual.ClientIdHeader);\n        Assert.Equal(hasOptions, actual.EnableRateLimiting);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.File;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitOptionsTests\n{\n    [Fact]\n    public void Ctor_Parameterless()\n    {\n        // Arrange, Act\n        RateLimitOptions actual = new();\n\n        // Assert\n        Assert.Equal(\"Oc-Client\", actual.ClientIdHeader);\n        Assert.Empty(actual.ClientWhitelist);\n        Assert.True(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(429, actual.StatusCode);\n        Assert.Equal(\"API calls quota exceeded! Maximum admitted {0} per {1}.\", actual.QuotaMessage);\n        Assert.Equal(\"Ocelot.RateLimiting\", actual.KeyPrefix);\n        Assert.Equal(RateLimitRule.Empty, actual.Rule);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Ctor_Boolean(bool enableRateLimiting)\n    {\n        // Arrange, Act\n        RateLimitOptions actual = new(enableRateLimiting);\n\n        // Assert\n        Assert.Equal(enableRateLimiting, actual.EnableRateLimiting);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void Ctor_Initialization(bool isEmpty)\n    {\n        // Arrange\n        string[] clientIdHeader = new[] { nameof(RateLimitOptions.ClientIdHeader), RateLimitOptions.DefaultClientHeader };\n        List<string>[] clientWhitelist = new[] { new List<string>([nameof(RateLimitOptions.ClientWhitelist)]), default };\n        bool[] enableHeaders = new[] { true, default };\n        bool[] enableRateLimiting = new[] { true, default };\n        string[] rateLimitCounterPrefix = new[] { nameof(RateLimitOptions.KeyPrefix), RateLimitOptions.DefaultCounterPrefix };\n        string[] quotaExceededMessage = new[] { nameof(RateLimitOptions.QuotaMessage), RateLimitOptions.DefaultQuotaMessage };\n        RateLimitRule[] rateLimitRule = new[] { new RateLimitRule(\"1\", \"2\", 3), default };\n        int[] httpStatusCode = new[] { 404, default };\n\n        // Act\n        int i = isEmpty ? 1 : 0;\n        RateLimitOptions actual = new(\n            enableRateLimiting[i],\n            clientIdHeader[i],\n            clientWhitelist[i],\n            enableHeaders[i],\n            quotaExceededMessage[i],\n            rateLimitCounterPrefix[i],\n            rateLimitRule[i],\n            httpStatusCode[i]);\n\n        // Assert\n        Assert.Equal(clientIdHeader[i], actual.ClientIdHeader);\n        Assert.Equal(clientWhitelist[i] ?? [], actual.ClientWhitelist);\n        Assert.Equal(enableHeaders[i], actual.EnableHeaders);\n        Assert.Equal(enableRateLimiting[i], actual.EnableRateLimiting);\n        Assert.Equal(rateLimitCounterPrefix[i], actual.KeyPrefix);\n        Assert.Equal(quotaExceededMessage[i], actual.QuotaMessage);\n        Assert.Equal(rateLimitCounterPrefix[i], actual.KeyPrefix);\n        Assert.Equal(rateLimitRule[i], actual.Rule);\n        Assert.Equal(httpStatusCode[i], actual.StatusCode);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFrom_ArgChecks()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule fromRule = null;\n\n        // Act, Assert\n        var ex = Assert.Throws<ArgumentNullException>(() => new RateLimitOptions(fromRule));\n        Assert.Equal(nameof(fromRule), ex.ParamName);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFrom_WithObsoleteProps()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule from = new()\n        {\n            ClientIdHeader = \"1\",\n            ClientWhitelist = [\"2\"],\n            DisableRateLimitHeaders = true,\n            EnableRateLimiting = true,\n            HttpStatusCode = 333,\n            QuotaExceededMessage = \"4\",\n            RateLimitCounterPrefix = \"5\",\n            Period = \"6\",\n            PeriodTimespan = 7D,\n            Limit = 8,\n        };\n\n        // Act\n        RateLimitOptions actual = new(from);\n\n        // Assert\n        Assert.Equal(\"1\", actual.ClientIdHeader);\n        Assert.Contains(\"2\", actual.ClientWhitelist);\n        Assert.False(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(333, actual.StatusCode);\n        Assert.Equal(\"4\", actual.QuotaMessage);\n        Assert.Equal(\"5\", actual.KeyPrefix);\n        Assert.Equal(\"8/6/w7s\", actual.Rule.ToString());\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFrom_FileRateLimitByHeaderRule()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule from = new()\n        {\n            ClientIdHeader = \"1\",\n            ClientWhitelist = [\"2\"],\n            DisableRateLimitHeaders = null,\n            EnableHeaders = true,\n            EnableRateLimiting = true,\n            HttpStatusCode = null,\n            StatusCode = 444,\n            QuotaExceededMessage = null,\n            QuotaMessage = \"55\",\n            RateLimitCounterPrefix = null,\n            KeyPrefix = \"66\",\n            Period = \"7\",\n            PeriodTimespan = null,\n            Wait = \"8\",\n            Limit = 9,\n        };\n\n        // Act\n        RateLimitOptions actual = new(from);\n\n        // Assert\n        Assert.Equal(\"1\", actual.ClientIdHeader);\n        Assert.Contains(\"2\", actual.ClientWhitelist);\n        Assert.True(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(444, actual.StatusCode);\n        Assert.Equal(\"55\", actual.QuotaMessage);\n        Assert.Equal(\"66\", actual.KeyPrefix);\n        Assert.Equal(\"9/7/w8\", actual.Rule.ToString());\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFromFileRateLimitByHeaderRule_WithDefaults()\n    {\n        // Arrange\n        FileRateLimitByHeaderRule from = new();\n\n        // Act\n        RateLimitOptions actual = new(from);\n\n        // Assert\n        Assert.Equal(\"Oc-Client\", actual.ClientIdHeader);\n        Assert.Empty(actual.ClientWhitelist);\n        Assert.True(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(429, actual.StatusCode);\n        Assert.Equal(\"API calls quota exceeded! Maximum admitted {0} per {1}.\", actual.QuotaMessage);\n        Assert.Equal(\"Ocelot.RateLimiting\", actual.KeyPrefix);\n        Assert.Equal(RateLimitRule.Empty.ToString(), actual.Rule.ToString());\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFrom_RateLimitOptions()\n    {\n        // Arrange\n        RateLimitOptions from = new()\n        {\n            ClientIdHeader = \"1\",\n            ClientWhitelist = [\"2\"],\n            EnableHeaders = true,\n            EnableRateLimiting = true,\n            StatusCode = 444,\n            QuotaMessage = \"55\",\n            KeyPrefix = \"66\",\n            Rule = new(\"7\", \"8\",9),\n        };\n\n        // Act\n        RateLimitOptions actual = new(from);\n\n        // Assert\n        Assert.Equal(\"1\", actual.ClientIdHeader);\n        Assert.Contains(\"2\", actual.ClientWhitelist);\n        Assert.True(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(444, actual.StatusCode);\n        Assert.Equal(\"55\", actual.QuotaMessage);\n        Assert.Equal(\"66\", actual.KeyPrefix);\n        Assert.Equal(\"9/7/w8\", actual.Rule.ToString());\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"1229\")]\n    [Trait(\"PR\", \"2294\")]\n    public void Ctor_CopyingFromRateLimitOptions_WithDefaults()\n    {\n        // Arrange\n        RateLimitOptions from = new()\n        {\n            ClientIdHeader = string.Empty,\n            ClientWhitelist = null,\n            QuotaMessage = string.Empty,\n            KeyPrefix = string.Empty,\n            Rule = null,\n        };\n\n        // Act\n        RateLimitOptions actual = new(from);\n\n        // Assert\n        Assert.Equal(\"Oc-Client\", actual.ClientIdHeader);\n        Assert.Empty(actual.ClientWhitelist);\n        Assert.True(actual.EnableHeaders);\n        Assert.True(actual.EnableRateLimiting);\n        Assert.Equal(429, actual.StatusCode);\n        Assert.Equal(\"API calls quota exceeded! Maximum admitted {0} per {1}.\", actual.QuotaMessage);\n        Assert.Equal(\"Ocelot.RateLimiting\", actual.KeyPrefix);\n        Assert.Equal(RateLimitRule.Empty.ToString(), actual.Rule.ToString());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitRuleTests.cs",
    "content": "﻿using Ocelot.Configuration;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\n[Trait(\"PR\", \"2294\")] // https://github.com/ThreeMammals/Ocelot/pull/2294\npublic class RateLimitRuleTests\n{\n    [Theory]\n    [Trait(\"Feat\", \"585\")] // https://github.com/ThreeMammals/Ocelot/issues/585\n    [Trait(\"Feat\", \"1915\")] // https://github.com/ThreeMammals/Ocelot/issues/1915\n    [InlineData(\"1ms\", 1D)]\n    [InlineData(\"1s\", 1_000D)]\n    [InlineData(\"1m\", 60_000D)]\n    [InlineData(\"1h\", 3_600_000D)]\n    [InlineData(\"1d\", 86_400_000D)]\n    public void ParseTimespan_ShouldParseSupportedUnits(string oneUnit, double expected)\n    {\n        TimeSpan expectedSpan = TimeSpan.FromMilliseconds(expected);\n        TimeSpan actual = RateLimitRule.ParseTimespan(oneUnit);\n        Assert.Equal(expectedSpan, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"1915\")]\n    [InlineData(\"1ms\", 1.0D)]\n    [InlineData(\"123ms\", 123.0D)]\n    [InlineData(\"2.ms\", 2.0D)]\n    [InlineData(\"3.0ms\", 3.0D)]\n    [InlineData(\".4ms\", 0.4D)]\n    [InlineData(\"0.5ms\", 0.5D)]\n    [InlineData(\"0.678ms\", 0.678D)]\n    [InlineData(\"123.456ms\", 123.456D)]\n    public void ParseTimespan_ShouldParseMilliseconds(string value, double ms)\n    {\n        TimeSpan expected = TimeSpan.FromMilliseconds(ms);\n        TimeSpan actual = RateLimitRule.ParseTimespan(value);\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"1915\")]\n    [InlineData(\"1\", 1.0D)]\n    [InlineData(\"123\", 123.0D)]\n    [InlineData(\"2.\", 2.0D)]\n    [InlineData(\"3.0\", 3.0D)]\n    [InlineData(\".4\", 0.4D)]\n    [InlineData(\"0.5\", 0.5D)]\n    [InlineData(\"0.678\", 0.678D)]\n    [InlineData(\"78.999\", 78.999D)]\n    public void ParseTimespan_ShouldParseWithoutUnitToMilliseconds(string value, double ms)\n    {\n        TimeSpan expected = TimeSpan.FromMilliseconds(ms);\n        TimeSpan actual = RateLimitRule.ParseTimespan(value);\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"1915\")]\n    [InlineData(\"-1ms\", 1D)]\n    [InlineData(\"-20s\", 20_000D)]\n    [InlineData(\"-3.0m\", 180_000D)]\n    public void ParseTimespan_ShouldParseNegativeAsPositive(string value, double ms)\n    {\n        TimeSpan expected = TimeSpan.FromMilliseconds(ms);\n        TimeSpan actual = RateLimitRule.ParseTimespan(value);\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"Feat\", \"1915\")]\n    [InlineData(\"1x\", \"The '1x' timespan cannot be converted to TimeSpan due to an unknown 'x' unit!\")]\n    [InlineData(\"x-bla-bla\", $\"The 'x-bla-bla' value doesn't include any digits, so it cannot be considered a number!\")]\n    public void ParseTimespan_ShouldThrowFormatException(string value, string message)\n    {\n        var error = Assert.Throws<FormatException>(() => RateLimitRule.ParseTimespan(value));\n        Assert.Equal(message, error.Message);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitingHeadersTests.cs",
    "content": "﻿using Ocelot.RateLimiting;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitingHeadersTests\n{\n    [Fact]\n    public void Cctor_PropsInitialized()\n    {\n        // Arrange, Act, Assert\n        Assert.Equal(\"Retry-After\", RateLimitingHeaders.Retry_After);\n        Assert.Equal(\"X-RateLimit-Limit\", RateLimitingHeaders.X_RateLimit_Limit);\n        Assert.Equal(\"X-RateLimit-Remaining\", RateLimitingHeaders.X_RateLimit_Remaining);\n        Assert.Equal(\"X-RateLimit-Reset\", RateLimitingHeaders.X_RateLimit_Reset);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Caching.Memory;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.RateLimiting;\nusing Ocelot.Request.Middleware;\nusing System.Reflection;\nusing System.Runtime.CompilerServices;\nusing System.Text;\nusing _DownstreamRouteHolder_ = Ocelot.DownstreamRouteFinder.DownstreamRouteHolder;\nusing _RateLimiting_ = Ocelot.RateLimiting.RateLimiting;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitingMiddlewareTests : UnitTest\n{\n    private readonly IRateLimitStorage _storage;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IHttpContextAccessor> _contextAccessor;\n    private readonly RateLimitingMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly IRateLimiting _rateLimiting;\n    private readonly List<DownstreamResponse> _downstreamResponses;\n    private readonly string _url;\n    private Func<string> _loggerMessage;\n\n    public RateLimitingMiddlewareTests()\n    {\n        _url = \"http://localhost:\" + PortFinder.GetRandomPort();\n        var cacheEntryOptions = new MemoryCacheOptions();\n        _storage = new MemoryCacheRateLimitStorage(new MemoryCache(cacheEntryOptions));\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _logger.Setup(x => x.LogInformation(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => _loggerMessage = f);\n        _loggerFactory.Setup(x => x.CreateLogger<RateLimitingMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _rateLimiting = new _RateLimiting_(_storage);\n        _contextAccessor = new Mock<IHttpContextAccessor>();\n        _middleware = new RateLimitingMiddleware(_next, _loggerFactory.Object, _rateLimiting, _contextAccessor.Object);\n        _downstreamResponses = new();\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    public async Task Should_call_middleware_and_ratelimiting()\n    {\n        // Arrange\n        const long limit = 3L;\n        var downstreamRoute = GivenDownstreamRoute(rule: new(\"1s\", \"100s\", limit));\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n\n        // Act, Assert\n        await WhenICallTheMiddlewareMultipleTimes(limit, dsHolder);\n        _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());\n\n        // Act, Assert: the next request should fail\n        await WhenICallTheMiddlewareMultipleTimes(3, dsHolder);\n        _downstreamResponses.ShouldNotBeNull();\n        for (int i = 0; i < _downstreamResponses.Count; i++)\n        {\n            var response = _downstreamResponses[i].ShouldNotBeNull();\n            response.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $\"Downstream Response no is {i}\");\n            var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n            body.ShouldBe(\"Exceeding!\");\n        }\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"37\")]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task Should_not_call_middleware_with_disabled_ratelimiting(bool hasOptions)\n    {\n        // Arrange\n        RateLimitOptions options = hasOptions ? new(false) : null;\n        var downstreamRoute = new DownstreamRouteBuilder()\n                .WithRateLimitOptions(options)\n                .Build();\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n\n        // Act\n        var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder);\n\n        // Assert\n        _downstreamResponses.ShouldNotBeNull();\n        _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());\n        ShouldLogInformation(\"Rate limiting is disabled for route '?' via the EnableRateLimiting option.\");\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    public async Task Should_call_middleware_with_whitelisted_client()\n    {\n        // Arrange\n        var opts = GivenRateLimitOptions(new(\"1s\", \"100s\", 3));\n        var options = new RateLimitOptions(opts)\n        {\n            ClientWhitelist = [\"ocelotclient2\"],\n        };\n        var downstreamRoute = GivenDownstreamRoute(options);\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n\n        // Act\n        await WhenICallTheMiddlewareWithWhiteClient(dsHolder);\n\n        // Assert\n        _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1305 \")] // https://github.com/ThreeMammals/Ocelot/issues/1305\n    [Trait(\"PR\", \"1307 \")] // https://github.com/ThreeMammals/Ocelot/pull/1307\n    public async Task ShouldPopulateRateLimitingHeaders()\n    {\n        // Arrange\n        RateLimitOptions options = new()\n        {\n            EnableHeaders = true,\n            ClientIdHeader = \"ClientId\",\n            Rule = new(\"1s\", \"1s\", 3),\n        };\n        var downstreamRoute = GivenDownstreamRoute(options);\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n        var originalContext = new DefaultHttpContext();\n        _contextAccessor.SetupGet(x => x.HttpContext).Returns(originalContext);\n\n        // Act\n        var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, null, originalContext);\n\n        // Assert\n        originalContext.Response.ShouldNotBeNull();\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()), Times.Once);\n        var msg = _loggerMessage.ShouldNotBeNull().Invoke();\n        msg.ShouldStartWith(\"Route '?' must return rate limiting headers with the following data: 2/3 resets at \"); // Route '?' must return rate limiting headers with the following data: 2/3 resets at 2025-09-11T13:37:13.7973731Z\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1305 \")]\n    [Trait(\"PR\", \"1307 \")]\n    [InlineData(false, false, 0)]\n    [InlineData(false, true, 0)]\n    [InlineData(true, false, 0)]\n    [InlineData(true, true, 1)]\n    public async Task ShouldPopulateRateLimitingHeaders_Branches(bool enableHeaders, bool hasContext, int loggedTimes)\n    {\n        // Arrange\n        RateLimitOptions options = new()\n        {\n            EnableHeaders = enableHeaders,\n            ClientIdHeader = \"ClientId\",\n            Rule = new(\"1s\", \"1s\", 3),\n        };\n        var downstreamRoute = GivenDownstreamRoute(options);\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n        var originalContext = hasContext ? new DefaultHttpContext() : null;\n        _contextAccessor.SetupGet(x => x.HttpContext).Returns(originalContext);\n\n        // Act\n        var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, null, originalContext);\n\n        // Assert\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()), Times.Exactly(loggedTimes));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1305 \")]\n    [Trait(\"PR\", \"1307 \")]\n    public async Task SetRateLimitHeaders()\n    {\n        // Arrange\n        var today = new DateTime(2025, 9, 11, 3, 4, 5, 6, 7, DateTimeKind.Utc);\n        var context = new DefaultHttpContext();\n        var state = new RateLimitHeaders(context, 3, 2, today);\n        var method = _middleware.GetType().GetMethod(\"SetRateLimitHeaders\", BindingFlags.Instance | BindingFlags.NonPublic);\n\n        // Act\n        Task t = method.Invoke(_middleware, [state]) as Task;\n        await t;\n\n        // Assert\n        Assert.True(t.IsCompleted);\n        var headers = context.Response.Headers;\n        Assert.NotEmpty(headers);\n        Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Limit));\n        Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Remaining));\n        Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Reset));\n        Assert.Equal(\"3\", headers[RateLimitingHeaders.X_RateLimit_Limit]);\n        Assert.Equal(\"2\", headers[RateLimitingHeaders.X_RateLimit_Remaining]);\n        Assert.Equal(\"2025-09-11T03:04:05.0060070Z\", headers[RateLimitingHeaders.X_RateLimit_Reset]);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1590\")]\n    public async Task Invoke_PeriodTimespanValueIsGreaterThanPeriod_StatusNotEqualTo429()\n    {\n        // Arrange\n        const long limit = 100L;\n        var rule = new RateLimitRule(\"1s\", \"30s\", limit); // bug scenario\n        var downstreamRoute = GivenDownstreamRoute(rule: rule);\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n\n        // Act, Assert: 100 requests must be successful\n        var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, dsHolder); // make 100 requests, but not exceed the limit\n        _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());\n        contexts.ForEach(ctx =>\n        {\n            ctx.ShouldNotBeNull();\n            ctx.Items.Errors().ShouldNotBeNull().ShouldBeEmpty(); // no errors\n            ctx.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); // not 429 aka TooManyRequests\n        });\n\n        // Act, Assert: the next 101st request should fail\n        contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder);\n        _downstreamResponses.ShouldNotBeNull();\n        var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull();\n        ds.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $\"Downstream Response no {limit + 1}\");\n        var body = await ds.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        body.ShouldBe(\"Exceeding!\");\n        contexts[0].Items.Errors().ShouldNotBeNull().ShouldNotBeEmpty(); // having errors\n        contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"37\")]\n    [Trait(\"Feat\", \"585\")]\n    [Trait(\"PR\", \"2294\")]\n    public async Task Invoke_NoClientHeader_Status503_ShouldLogWarning()\n    {\n        // Arrange\n        var downstreamRoute = GivenDownstreamRoute();\n        var route = GivenRoute(downstreamRoute);\n        var dsHolder = new _DownstreamRouteHolder_(new(), route);\n\n        // Act\n        var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, \"bla-bla-header:spy\");\n\n        // Assert\n        var ctx = contexts[0].ShouldNotBeNull();\n        var errors = ctx.Items.Errors().ShouldNotBeNull();\n        var err = Assert.Single(errors);\n        Assert.IsType<QuotaExceededError>(err);\n        Assert.Equal(\"Rate limiting client could not be identified for the route '?' due to a missing or unknown client ID header required by rule '3/1s/w1s'!\", err.Message);\n        var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull();\n        Assert.Equal(HttpStatusCode.ServiceUnavailable, ds.StatusCode);\n        var body = await ds.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        Assert.Equal(\"Rate limiting client could not be identified for the route '?' due to a missing or unknown client ID header required by rule '3/1s/w1s'!\", body);\n        _logger.Verify(x => x.LogWarning(err.Message), Times.Once);\n    }\n\n    private static RateLimitOptions GivenRateLimitOptions(RateLimitRule rule = null, [CallerMemberName] string testName = null) => new(\n            enableRateLimiting: true,\n            clientIdHeader: \"ClientId\",\n            clientWhitelist: [],\n            enableHeaders: true,\n            quotaExceededMessage: \"Exceeding!\",\n            rateLimitCounterPrefix: testName,\n            rule ?? new(\"1s\", \"1s\", 3),\n            StatusCodes.Status429TooManyRequests);\n\n    private static DownstreamRoute GivenDownstreamRoute(RateLimitOptions options = null, RateLimitRule rule = null, [CallerMemberName] string testName = null)\n        => new DownstreamRouteBuilder()\n        .WithRateLimitOptions(options ?? GivenRateLimitOptions(rule))\n        .WithUpstreamHttpMethod([HttpMethods.Get])\n        .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build())\n        .WithLoadBalancerKey(testName)\n        .Build();\n\n    private static Route GivenRoute(DownstreamRoute dr) => new()\n    {\n        DownstreamRoute = [dr],\n        UpstreamHttpMethod = [HttpMethod.Get],\n    };\n\n    private async Task<List<HttpContext>> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ holder, string header = null, HttpContext originalContext = null)\n    {\n        var contexts = new List<HttpContext>();\n        _downstreamResponses.Clear();\n        for (var i = 0; i < times; i++)\n        {\n            var context = originalContext ?? new DefaultHttpContext();\n            var stream = GetFakeStream($\"{i}\");\n            context.Response.Body = stream;\n            context.Response.RegisterForDispose(stream);\n            context.Items.UpsertDownstreamRoute(holder.Route.DownstreamRoute[0]);\n            context.Items.UpsertTemplatePlaceholderNameAndValues(holder.TemplatePlaceholderNameAndValues);\n            context.Items.UpsertDownstreamRoute(holder);\n            var request = new HttpRequestMessage(new HttpMethod(\"GET\"), _url);\n            context.Items.UpsertDownstreamRequest(new DownstreamRequest(request));\n            header ??= \"ClientId:ocelotclient1\";\n            var hdr = header.Split(':');\n            context.Request.Headers.TryAdd(hdr[0], hdr[1]);\n            contexts.Add(context);\n\n            await _middleware.Invoke(context);\n\n            _downstreamResponses.Add(context.Items.DownstreamResponse());\n        }\n\n        return contexts;\n    }\n\n    private static MemoryStream GetFakeStream(string str)\n    {\n        byte[] data = Encoding.ASCII.GetBytes(str);\n        return new MemoryStream(data, 0, data.Length);\n    }\n\n    private async Task WhenICallTheMiddlewareWithWhiteClient(_DownstreamRouteHolder_ holder)\n    {\n        const string ClientId = \"ocelotclient2\";\n        for (var i = 0; i < 10; i++)\n        {\n            var context = new DefaultHttpContext();\n            var stream = GetFakeStream($\"{i}\");\n            context.Response.Body = stream;\n            context.Response.RegisterForDispose(stream);\n            context.Items.UpsertDownstreamRoute(holder.Route.DownstreamRoute[0]);\n            context.Items.UpsertTemplatePlaceholderNameAndValues(holder.TemplatePlaceholderNameAndValues);\n            context.Items.UpsertDownstreamRoute(holder);\n            var request = new HttpRequestMessage(new HttpMethod(\"GET\"), _url);\n            request.Headers.Add(\"ClientId\", ClientId);\n            context.Items.UpsertDownstreamRequest(new DownstreamRequest(request));\n            context.Request.Headers.TryAdd(\"ClientId\", ClientId);\n\n            await _middleware.Invoke(context);\n\n            _downstreamResponses.Add(context.Items.DownstreamResponse());\n        }\n    }\n\n    private void ShouldLogInformation(string expected)\n    {\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()), Times.Once);\n        var msg = _loggerMessage.ShouldNotBeNull().Invoke();\n        msg.ShouldBe(expected);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.RateLimiting;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\nusing _RateLimiting_ = Ocelot.RateLimiting.RateLimiting;\n\nnamespace Ocelot.UnitTests.RateLimiting;\n\npublic class RateLimitingTests : RateLimitingTestsBase\n{\n    [Theory]\n    [Trait(\"Feat\", \"37\")]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public void ToTimespan_EmptyValue_ShouldReturnZero(string empty)\n    {\n        // Arrange, Act\n        var actual = _sut.ToTimespan(empty);\n\n        // Assert\n        Assert.Equal(TimeSpan.Zero, actual);\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"37\")]\n    [InlineData(\"1a\")]\n    [InlineData(\"2unknown\")]\n    public void ToTimespan_UnknownType_ShouldThrowFormatException(string timespan)\n    {\n        // Arrange, Act, Assert\n        Assert.Throws<FormatException>(\n            () => _sut.ToTimespan(timespan));\n    }\n\n    [Theory]\n    [Trait(\"Feat\", \"37\")]\n    [InlineData(\"1s\", 1 * TimeSpan.TicksPerSecond)]\n    [InlineData(\"2m\", 2 * TimeSpan.TicksPerMinute)]\n    [InlineData(\"3h\", 3 * TimeSpan.TicksPerHour)]\n    [InlineData(\"4d\", 4 * TimeSpan.TicksPerDay)]\n    public void ToTimespan_KnownType_HappyPath(string timespan, long ticks)\n    {\n        // Arrange\n        var expected = TimeSpan.FromTicks(ticks);\n\n        // Act\n        var actual = _sut.ToTimespan(timespan);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void Count_NoEntry_StartCounting()\n    {\n        // Arrange\n        DateTime now = DateTime.UtcNow;\n        RateLimitCounter? arg1 = null; // No Entry\n        RateLimitRule arg2 = null;\n\n        // Act\n        RateLimitCounter actual = _sut.Count(arg1, arg2, now);\n\n        // Assert\n        Assert.Equal(1L, actual.Total);\n        Assert.True(now - actual.StartedAt < TimeSpan.FromSeconds(1.0D));\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void Count_EntryHasNotExpired_IncrementedRequestCount()\n    {\n        // Arrange\n        long total = 2;\n        DateTime now = DateTime.UtcNow;\n        RateLimitCounter? arg1 = new RateLimitCounter(now, null, total); // entry has not expired\n        RateLimitRule arg2 = new(\"1s\", \"1s\", total + 1); // with not exceeding limit\n\n        // Act\n        RateLimitCounter actual = _sut.Count(arg1, arg2, now);\n\n        // Assert\n        Assert.Equal(total + 1, actual.Total); // incremented request count\n        Assert.Equal(arg1.Value.StartedAt, actual.StartedAt); // starting point has not changed\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void Count_EntryHasNotExpiredAndExceedingLimit_IncrementedRequestCountWithRenewedStartMoment()\n    {\n        // Arrange\n        long total = 2;\n        DateTime now = DateTime.UtcNow;\n        RateLimitCounter? arg1 = new RateLimitCounter(now, null, total); // entry has not expired\n        RateLimitRule arg2 = new(\"1s\", \"1s\", 1L);\n\n        // Act\n        RateLimitCounter actual = _sut.Count(arg1, arg2, now);\n\n        // Assert\n        Assert.Equal(total + 1, actual.Total); // incremented request count\n        Assert.InRange(actual.StartedAt, arg1.Value.StartedAt, now); // starting point has renewed and it is between StartedAt and Now\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void Count_RateLimitExceeded_StartedCounting()\n    {\n        // Arrange\n        long total = 3, limit = total - 1;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", \"2s\", limit); // rate limit exceeded\n        var entry = new RateLimitCounter(\n            now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds),\n            now.AddSeconds(-rule.WaitSpan.TotalSeconds),\n            total); // Entry has expired\n\n        // Act\n        var futureIsNow = now.AddMilliseconds(1); // let's move to the future to allow the waiting period to pass\n        RateLimitCounter actual = _sut.Count(entry, rule, futureIsNow);\n\n        // Assert\n        Assert.Equal(1L, actual.Total); // started counting, the counter was changed\n        Assert.Equal(futureIsNow, actual.StartedAt); // started now\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void Count_PeriodIsElapsedAndWaitPeriodIsElapsed_StartedNewCountingPeriod()\n    {\n        // Arrange\n        long total = 3, limit = 3;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", \"1s\", limit);\n        RateLimitCounter? entry = new(\n            now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds), // 2 seconds ago\n            now.AddSeconds(-rule.WaitSpan.TotalSeconds), // 1 second ago\n            total); // Entry is about to expire\n\n        // Act, Assert 1\n        RateLimitCounter actual = _sut.Count(entry, rule, now); // at the moment of wait period elapsing, inclusively\n        Assert.Equal(4, actual.Total); // started counting\n        Assert.True(actual.ExceededAt.HasValue); // old counter is valid\n\n        // Act, Assert 2\n        var futureIsNow = now.AddMilliseconds(1); // let's move to the future to allow the waiting period to pass\n        actual = _sut.Count(entry, rule, futureIsNow);\n        Assert.Equal(1L, actual.Total); // started counting\n        Assert.Equal(futureIsNow, actual.StartedAt); // started now\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1592\")]\n    public void ProcessRequest_QuotaExceededAndWaitPeriodElapsed_StartedCountingViaResettingCounter()\n    {\n        // Arrange\n        const string fixedWindow = \"3s\", waitWindow = \"2s\";\n        RateLimitRule rule = new(fixedWindow, waitWindow, 2);\n        DateTime now = DateTime.UtcNow, startedAt = now.AddSeconds(-rule.PeriodSpan.TotalSeconds);\n        DateTime? exceededAt = null;\n        long totalRequests = 2L;\n        TimeSpan expiration = TimeSpan.Zero;\n\n        var (identity, options) = SetupProcessRequest(fixedWindow, waitWindow, totalRequests,\n            () => new RateLimitCounter(startedAt, exceededAt, totalRequests),\n            (value) => expiration = value);\n\n        // Act 1\n        var counter = _sut.ProcessRequest(identity, options, now);\n\n        // Assert 1\n        Assert.Equal(3L, counter.Total); // old counting -> 3\n        Assert.Equal(startedAt, counter.StartedAt); // starting point was not changed\n        Assert.True(counter.ExceededAt.HasValue); // exceeded\n        Assert.Equal(now, counter.ExceededAt.Value); // exceeded now, in the same second\n\n        // Arrange 2\n        startedAt = counter.StartedAt; // move to past\n        exceededAt = counter.ExceededAt; // move to past\n        totalRequests = counter.Total; // 3\n        now += rule.WaitSpan; // don't wait, just move to future\n\n        // Act 2\n        var actual = _sut.ProcessRequest(identity, options, now);\n\n        // Assert\n        Assert.Equal(1L, actual.Total); // started counting\n        Assert.Equal(actual.StartedAt, now); // starting point has renewed and it is Now\n        Assert.Null(actual.ExceededAt);\n        _storage.Verify(x => x.Remove(It.IsAny<string>()),\n            Times.Never()); // Once()? Seems Remove is never called because of renewing\n        _storage.Verify(x => x.Get(It.IsAny<string>()),\n            Times.Exactly(2));\n        _storage.Verify(x => x.Set(It.IsAny<string>(), It.IsAny<RateLimitCounter>(), It.IsAny<TimeSpan>()),\n            Times.Exactly(2));\n        Assert.Equal(6, expiration.TotalSeconds);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2294\")]\n    public void RetryAfter_NoQuotaExceeding_NoNeedToRetry()\n    {\n        // Arrange\n        long total = 2, limit = 3;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", \"1s\", limit);\n        RateLimitCounter counter = new(\n            now.AddSeconds(-rule.PeriodSpan.TotalSeconds / 2),\n            null, total);\n\n        // Act\n        double actual = _sut.RetryAfter(counter, rule, now);\n\n        // Assert\n        Assert.Equal(0.0, actual);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2294\")]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(RateLimitRule.ZeroWait)]\n    public void RetryAfter_DoNotWait_RetryAfterTheHalfOfPeriod(string doNotWait)\n    {\n        // Arrange\n        long total = 4, limit = 3;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", doNotWait, limit);\n        RateLimitCounter counter = new(\n            startedAt: now.AddSeconds(-rule.PeriodSpan.TotalSeconds / 2),\n            exceededAt: now,\n            totalRequests: total);\n\n        // Act\n        double actual = _sut.RetryAfter(counter, rule, now);\n\n        // Assert\n        Assert.Equal(0.5, actual);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2294\")]\n    public void RetryAfter_ExceedingInWaitingWindow_RetryAfterTheQuarterOfWaitPeriod()\n    {\n        // Arrange\n        long total = 4, limit = 3;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", \"1s\", limit);\n        RateLimitCounter counter = new(\n            startedAt: now.AddSeconds(-(rule.PeriodSpan.TotalSeconds / 2) - (rule.WaitSpan.TotalSeconds / 4 * 3)),\n            exceededAt: now.AddSeconds(-(rule.WaitSpan.TotalSeconds / 4 * 3)),\n            totalRequests: total);\n\n        // Act\n        double actual = _sut.RetryAfter(counter, rule, now);\n\n        // Assert\n        Assert.Equal(0.25, actual);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"2294\")]\n    public void RetryAfter_Exceeding_WaitingPeriodElapsed_NoNeedToRetry()\n    {\n        // Arrange\n        long total = 4, limit = 3;\n        DateTime now = DateTime.UtcNow;\n        RateLimitRule rule = new(\"1s\", \"1s\", limit);\n        RateLimitCounter counter = new(\n            startedAt: now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds),\n            exceededAt: now.AddSeconds(-rule.WaitSpan.TotalSeconds),\n            totalRequests: total);\n\n        // Act\n        double actual = _sut.RetryAfter(counter, rule, now);\n\n        // Assert\n        Assert.Equal(-1.0, actual);\n    }\n\n    [Collection(nameof(SequentialTests))]\n    public class Sequential : RateLimitingTestsBase\n    {\n        [Fact]\n        [Trait(\"Bug\", \"1590\")]\n        public void ProcessRequest_PeriodTimespanValueIsGreaterThanPeriod_ExpectedBehaviorAndExpirationInPeriod()\n        {\n            // The test is stable in Linux and Windows only\n            Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), \"Skip in MacOS because the test is very unstable\");\n\n            // Arrange: user scenario\n            const long limit = 100L, requestsPerSecond = 20L;\n            const string fixedWindow = \"1s\", waitWindow = \"30s\";\n            RateLimitRule rule = new(fixedWindow, waitWindow, 2);\n\n            // Arrange: setup\n            DateTime now = DateTime.UtcNow;\n            DateTime? startedAt = null;\n            TimeSpan expiration = TimeSpan.Zero;\n            long total = 1L, count = requestsPerSecond;\n            RateLimitCounter? current = null;\n            \n            var (identity, options) = SetupProcessRequest(fixedWindow, waitWindow, limit,\n                () => current,\n                (value) => expiration = value);\n\n            // Arrange 20 requests per period (1 sec)\n            var periodMilliseconds = rule.PeriodSpan.TotalMilliseconds;\n            int delay = (int)((periodMilliseconds - 200) / requestsPerSecond); // 20 requests per 1 second\n\n            while (count > 0L)\n            {\n                // Act\n                var actual = _sut.ProcessRequest(identity, options, now);\n\n                // life hack for the 1st request\n                if (count == requestsPerSecond)\n                {\n                    startedAt = actual.StartedAt; // for the 1st request get expected value\n                }\n\n                // Assert\n                Assert.True(actual.Total < limit);\n                actual.Total.ShouldBe(total++, $\"Count is {count}\");\n                Assert.Equal(startedAt, actual.StartedAt); // starting point is not changed\n                Assert.Null(actual.ExceededAt); // no exceeding at all\n                Assert.Equal(32, expiration.TotalSeconds);\n\n                // Arrange: next micro test\n                current = actual;\n                Thread.Sleep(delay);\n                count--;\n            }\n\n            Assert.NotEqual(rule.WaitSpan, expiration); // Not Wait period expiration\n            Assert.Equal(32, expiration.TotalSeconds); // last 20th request was in counting period\n        }\n    }\n}\n\npublic class RateLimitingTestsBase\n{\n    protected readonly Mock<IRateLimitStorage> _storage;\n    protected readonly _RateLimiting_ _sut;\n    public RateLimitingTestsBase()\n    {\n        _storage = new();\n        _sut = new(_storage.Object);\n    }\n\n    protected (ClientRequestIdentity Identity, RateLimitOptions Options) SetupProcessRequest(string fixedWindow, string waitWindow, long limit,\n        Func<RateLimitCounter?> counterFactory, Action<TimeSpan> expirationAction, [CallerMemberName] string testName = \"\")\n    {\n        ClientRequestIdentity identity = new(nameof(RateLimitingTests) + \"/\" + testName, HttpMethods.Get);\n        RateLimitOptions options = new()\n        {\n            EnableRateLimiting = true,\n            KeyPrefix = nameof(_RateLimiting_.ProcessRequest),\n            Rule = new(fixedWindow, waitWindow, limit),\n        };\n        _storage.Setup(x => x.Get(It.IsAny<string>()))\n            .Returns(counterFactory); // counter value factory\n        _storage.Setup(x => x.Remove(It.IsAny<string>()))\n            .Verifiable();\n        expirationAction?.Invoke(TimeSpan.Zero);\n        _storage.Setup(x => x.Set(It.IsAny<string>(), It.IsAny<RateLimitCounter>(), It.IsAny<TimeSpan>()))\n            .Callback<string, RateLimitCounter, TimeSpan>((id, counter, expirationTime) => expirationAction?.Invoke(expirationTime))\n            .Verifiable();\n        return (identity, options);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Repository/HttpDataRepositoryTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Infrastructure.RequestData;\n\nnamespace Ocelot.UnitTests.Repository;\n\npublic class HttpDataRepositoryTests : UnitTest\n{\n    private readonly HttpDataRepository _repository;\n    private readonly HttpContextAccessor _contextAccesor;\n\n    public HttpDataRepositoryTests()\n    {\n        _contextAccesor = new()\n        {\n            HttpContext = new DefaultHttpContext(),\n        };\n        _repository = new HttpDataRepository(_contextAccesor);\n    }\n\n    [Fact]\n    public void Should_add_item()\n    {\n        // Arrange\n        const string key = \"blahh\";\n        var toAdd = new[] { 1, 2, 3, 4 };\n\n        // Act\n        _repository.Add(key, toAdd);\n\n        // Assert\n        _contextAccesor.HttpContext.Items.TryGetValue(key, out var obj).ShouldBeTrue();\n        obj.ShouldNotBeNull();\n        var arr = (int[])obj;\n        arr.ShouldNotBeNull();\n        arr.ShouldContain(4);\n    }\n\n    [Fact]\n    public void Should_get_item()\n    {\n        // Arrange\n        const string key = \"chest\";\n        var data = new[] { 5435345 };\n        _contextAccesor.HttpContext.Items.Add(key, data);\n\n        // Act\n        var result = _repository.Get<int[]>(key);\n\n        // Assert\n        result.IsError.ShouldBeFalse();\n        result.Data.ShouldNotBeNull();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs",
    "content": "using Ocelot.Infrastructure;\nusing Ocelot.Request.Creator;\n\nnamespace Ocelot.UnitTests.Request.Creator;\n\npublic class DownstreamRequestCreatorTests : UnitTest\n{\n    private readonly Mock<IFrameworkDescription> _framework;\n    private readonly DownstreamRequestCreator _downstreamRequestCreator;\n\n    public DownstreamRequestCreatorTests()\n    {\n        _framework = new Mock<IFrameworkDescription>();\n        _downstreamRequestCreator = new DownstreamRequestCreator(_framework.Object);\n    }\n\n    [Fact]\n    public async Task Should_create_downstream_request()\n    {\n        // Arrange\n        var request = new HttpRequestMessage(HttpMethod.Get, \"http://www.test.com\");\n        var content = new StringContent(\"test\");\n        request.Content = content;\n        _framework.Setup(x => x.Get()).Returns(string.Empty);\n\n        // Act\n        var result = _downstreamRequestCreator.Create(request);\n\n        // Assert: Then The Downstream Request Has A Body\n        result.ShouldNotBeNull();\n        result.Method.ToLower().ShouldBe(\"get\");\n        result.Scheme.ToLower().ShouldBe(\"http\");\n        result.Host.ToLower().ShouldBe(\"www.test.com\");\n        var resultContent = await result.ToHttpRequestMessage().Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        resultContent.ShouldBe(\"test\");\n    }\n\n    [Fact]\n    public void Should_remove_body_for_http_methods()\n    {\n        // Arrange\n        var methods = new List<HttpMethod> { HttpMethod.Get, HttpMethod.Head, HttpMethod.Delete, HttpMethod.Trace };\n        var request = new HttpRequestMessage(HttpMethod.Get, \"http://www.test.com\");\n        var content = new StringContent(\"test\");\n        request.Content = content;\n\n        methods.ForEach(m =>\n        {\n            _framework.Setup(x => x.Get()).Returns(\".NET Framework\");\n\n            // Act\n            var result = _downstreamRequestCreator.Create(request);\n\n            // Assert: Then The Downstream Request Does Not Have A Body\n            result.ShouldNotBeNull();\n            result.Method.ToLower().ShouldBe(\"get\");\n            result.Scheme.ToLower().ShouldBe(\"http\");\n            result.Host.ToLower().ShouldBe(\"www.test.com\");\n            result.ToHttpRequestMessage().Content.ShouldBeNull();\n        });\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Infrastructure;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Creator;\nusing Ocelot.Request.Mapper;\nusing Ocelot.Request.Middleware;\n\nnamespace Ocelot.UnitTests.Request;\n\npublic class DownstreamRequestInitialiserMiddlewareTests : UnitTest\n{\n    private readonly DownstreamRequestInitialiserMiddleware _middleware;\n    private readonly DefaultHttpContext _httpContext;\n    private readonly Mock<RequestDelegate> _next;\n    private readonly Mock<IRequestMapper> _requestMapper;\n    private HttpRequestMessage _mappedRequest;\n    private readonly Exception _testException;\n\n    public DownstreamRequestInitialiserMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _requestMapper = new Mock<IRequestMapper>();\n        _next = new Mock<RequestDelegate>();\n        var logger = new Mock<IOcelotLogger>();\n        _testException = new Exception(\"test exception\");\n\n        var loggerFactory = new Mock<IOcelotLoggerFactory>();\n        loggerFactory.Setup(lf => lf.CreateLogger<DownstreamRequestInitialiserMiddleware>())\n            .Returns(logger.Object);\n\n        _middleware = new DownstreamRequestInitialiserMiddleware(\n            _next.Object,\n            loggerFactory.Object,\n            _requestMapper.Object,\n            new DownstreamRequestCreator(new FrameworkDescription()));\n\n        _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build());\n    }\n\n    [Fact]\n    public async Task Should_handle_valid_httpRequest()\n    {\n        // Arrange\n        GivenTheMapperWillReturnAMappedRequest();\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheContexRequestIsMappedToADownstreamRequest();\n        ThenTheDownstreamRequestIsStored();\n        ThenTheNextMiddlewareIsInvoked();\n        ThenTheDownstreamRequestMethodIs(\"GET\");\n    }\n\n    [Fact]\n    public async Task Should_map_downstream_route_method_to_downstream_request()\n    {\n        // Arrange\n        GivenTheMapperWillReturnAMappedRequest();\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheContexRequestIsMappedToADownstreamRequest();\n        ThenTheDownstreamRequestIsStored();\n        ThenTheNextMiddlewareIsInvoked();\n        ThenTheDownstreamRequestMethodIs(\"GET\");\n    }\n\n    [Fact]\n    public async Task Should_handle_mapping_failure()\n    {\n        // Arrange\n        _requestMapper.Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamRoute>()))\n            .Throws(_testException);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.DownstreamRequest().ShouldBeNull();\n        _httpContext.Items.Errors().Count.ShouldBe(1);\n        _httpContext.Items.Errors().First().ShouldBeOfType<UnmappableRequestError>();\n        _httpContext.Items.Errors().First().Message.ShouldBe($\"Error when parsing incoming request, exception: {_testException}\");\n        _next.Verify(n => n(It.IsAny<HttpContext>()), Times.Never);\n    }\n\n    private void ThenTheDownstreamRequestMethodIs(string expected)\n    {\n        _httpContext.Items.DownstreamRequest().Method.ShouldBe(expected);\n    }\n\n    private void GivenTheMapperWillReturnAMappedRequest()\n    {\n        _mappedRequest = new HttpRequestMessage(HttpMethod.Get, \"http://www.bbc.co.uk\");\n\n        _requestMapper\n            .Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamRoute>()))\n            .Returns(_mappedRequest);\n    }\n\n    private void ThenTheContexRequestIsMappedToADownstreamRequest()\n    {\n        _requestMapper.Verify(rm => rm.Map(_httpContext.Request, _httpContext.Items.DownstreamRoute()), Times.Once);\n    }\n\n    private void ThenTheDownstreamRequestIsStored()\n    {\n        _httpContext.Items.DownstreamRequest().ShouldNotBeNull();\n    }\n\n    private void ThenTheNextMiddlewareIsInvoked()\n    {\n        _next.Verify(n => n(_httpContext), Times.Once);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs",
    "content": "using Ocelot.Request.Middleware;\n\nnamespace Ocelot.UnitTests.Request;\n\npublic class DownstreamRequestTests\n{\n    [Fact]\n    public void Should_have_question_mark_with_question_mark_prefixed()\n    {\n        // Arrange\n        var requestMessage = new HttpRequestMessage\n        {\n            RequestUri = new Uri(\"https://example.com/a?b=c\"),\n        };\n        var downstreamRequest = new DownstreamRequest(requestMessage);\n\n        // Act\n        var result = downstreamRequest.ToHttpRequestMessage();\n\n        // Assert\n        result.RequestUri.Query.ShouldBe(\"?b=c\");\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Request.Mapper;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Ocelot.UnitTests.Request.Mapper;\n\npublic class RequestMapperTests : UnitTest\n{\n    private readonly HttpRequest _inputRequest;\n    private readonly RequestMapper _requestMapper;\n    private HttpRequestMessage _mappedRequest;\n    private List<KeyValuePair<string, StringValues>> _inputHeaders;\n    private DownstreamRoute _downstreamRoute;\n\n    public RequestMapperTests()\n    {\n        var httpContext = new DefaultHttpContext();\n        _inputRequest = httpContext.Request;\n        _requestMapper = new RequestMapper();\n    }\n\n    [Theory]\n    [InlineData(\"https\", \"my.url:123\", \"/abc/DEF\", \"?a=1&b=2\", \"https://my.url:123/abc/DEF?a=1&b=2\")]\n    [InlineData(\"http\", \"blah.com\", \"/d ef\", \"?abc=123\",\n        \"http://blah.com/d%20ef?abc=123\")] // note! the input is encoded when building the input request\n    [InlineData(\"http\", \"myusername:mypassword@abc.co.uk\", null, null, \"http://myusername:mypassword@abc.co.uk/\")]\n    [InlineData(\"http\", \"點看.com\", null, null, \"http://xn--c1yn36f.com/\")]\n    [InlineData(\"http\", \"xn--c1yn36f.com\", null, null, \"http://xn--c1yn36f.com/\")]\n    public void Should_map_valid_request_uri(string scheme, string host, string path, string queryString, string expectedUri)\n    {\n        // Arrange\r\n        _inputRequest.Method = \"GET\";\r\n        _inputRequest.Scheme = scheme;\r\n        GivenTheInputRequestHasHost(host);\r\n        GivenTheInputRequestHasPath(path);\r\n        GivenTheInputRequestHasQueryString(queryString);\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        Assert.NotNull(_mappedRequest.RequestUri);\n        _mappedRequest.RequestUri.OriginalString.ShouldBe(expectedUri);\n    }\n\n    [Theory]\n    [InlineData(\"ftp\", \"google.com\", \"/abc/DEF\", \"?a=1&b=2\")]\n    public void Should_error_on_unsupported_request_uri(string scheme, string host, string path, string queryString)\n    {\n        // Arrange\r\n        _inputRequest.Method = \"GET\";\r\n        _inputRequest.Scheme = scheme;\r\n        GivenTheInputRequestHasHost(host);\r\n        GivenTheInputRequestHasPath(path);\r\n        GivenTheInputRequestHasQueryString(queryString);\r\n\r\n        // Act, Assert\r\n        Assert.Throws<NullReferenceException>(() => _requestMapper.Map(_inputRequest, _downstreamRoute));\n    }\n\n    [Theory]\n    [InlineData(\"GET\")]\n    [InlineData(\"POST\")]\n    [InlineData(\"WHATEVER\")]\n    public void Should_map_method(string method)\n    {\n        // Arrange\r\n        _inputRequest.Method = method;\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Method.ToString().ShouldBe(method);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"GET\")]\n    [InlineData(null, \"GET\")]\n    [InlineData(\"POST\", \"POST\")]\n    public void Should_use_downstream_route_method_if_set(string input, string expected)\n    {\n        // Arrange\r\n        _inputRequest.Method = \"GET\";\r\n        _downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownStreamHttpMethod(input)\n            .WithDownstreamHttpVersion(new Version(\"1.1\"))\r\n            .Build();\n        GivenTheInputRequestHasAValidUri();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Method.ToString().ShouldBe(expected);\n    }\n\n    [Fact]\n    public void Should_map_all_headers()\n    {\n        // Arrange: Given The Input Request Has Headers\r\n        var abcVals = new[] { \"123\", \"456\" };\r\n        var defVals = new[] { \"789\", \"012\" };\n        _inputHeaders = new()\n        {\n            new(\"abc\", new StringValues(abcVals)),\n            new(\"def\", new StringValues(defVals)),\n        };\n\n        foreach (var inputHeader in _inputHeaders)\n        {\n            _inputRequest.Headers.Add(inputHeader);\n        }\n\r\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert: Then The Mapped Request Has Each Header\n        _mappedRequest.Headers.Count().ShouldBe(_inputHeaders.Count);\n        foreach (var header in _mappedRequest.Headers)\n        {\n            var inputHeader = _inputHeaders.First(h => h.Key == header.Key);\n            inputHeader.ShouldNotBe(default);\n            inputHeader.Value.Count.ShouldBe(header.Value.Count());\n            foreach (var inputHeaderValue in inputHeader.Value)\n            {\n                Assert.Contains(header.Value, v => v == inputHeaderValue);\n            }\n        }\n    }\n\n    [Fact]\n    public void Should_handle_no_headers()\n    {\n        // Arrange\r\n        _inputRequest.Headers.Clear();\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Headers.Count().ShouldBe(0);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"1972\")]\n    [InlineData(\"GET\")]\n    [InlineData(\"POST\")]\n    public async Task Should_map_content(string method)\n    {\n        // Arrange\r\n        GivenTheInputRequestHasContent(\"This is my content\");\r\n        _inputRequest.Method = method;\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        await ThenTheMappedRequestHasContent(\"This is my content\");\r\n        ThenTheMappedRequestHasContentLength(\"This is my content\".Length);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1972\")]\n    public async Task Should_map_chucked_content()\n    {\n        // Arrange\r\n        GivenTheInputRequestHasChunkedContent(\"This\", \" is my content\");\r\n        _inputRequest.Method = \"POST\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        await ThenTheMappedRequestHasContent(\"This is my content\");\r\n        _mappedRequest.Headers.TryGetValues(HeaderNames.ContentLength, out _).ShouldBeFalse(); // ThenTheMappedRequestHasNoContentLength\r\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1972\")]\n    public async Task Should_map_empty_content()\n    {\n        // Arrange\r\n        GivenTheInputRequestHasContent(\"\");\r\n        _inputRequest.Method = \"POST\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        await ThenTheMappedRequestHasContent(\"\");\r\n        ThenTheMappedRequestHasContentLength(0);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1972\")]\n    public async Task Should_map_empty_chucked_content()\n    {\n        // Arrange\r\n        GivenTheInputRequestHasChunkedContent();\r\n        _inputRequest.Method = \"POST\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        await ThenTheMappedRequestHasContent(\"\");\r\n        _mappedRequest.Headers.TryGetValues(HeaderNames.ContentLength, out _).ShouldBeFalse(); // ThenTheMappedRequestHasNoContentLength\r\n    }\n\n    [Fact]\n    public void Should_handle_no_content()\n    {\n        // Arrange\r\n        _inputRequest.Body = null!;\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Content.ShouldBeNull();\n    }\n\n    [Fact]\n    public void Should_handle_no_content_type()\n    {\n        // Arrange\r\n        _inputRequest.ContentType = null;\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Content.ShouldBeNull();\n    }\n\n    [Fact]\n    public void Should_handle_no_content_length()\n    {\n        // Arrange\r\n        _inputRequest.ContentLength = null;\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        _mappedRequest.Content.ShouldBeNull();\n    }\n\n    [Fact]\n    public void Should_map_content_headers()\n    {\r\n        // Arrange\r\n        var bytes = Encoding.UTF8.GetBytes(\"some md5\");\n        var md5Bytes = MD5.HashData(bytes);\n\n        GivenTheInputRequestHasContent(\"This is my content\");\r\n        _inputRequest.ContentType = \"application/json\";\r\n        _inputRequest.Headers.Append(\"Content-Encoding\", \"gzip, compress\");\n        _inputRequest.Headers.Append(\"Content-Language\", \"english\");\n        _inputRequest.Headers.Append(\"Content-Location\", \"/my-receipts/38\");\n        _inputRequest.Headers.Append(\"Content-Range\", \"bytes 1-2/*\");\n        _inputRequest.Headers.Append(\"Content-Disposition\", \"inline\");\n        var base64 = Convert.ToBase64String(md5Bytes);\n        _inputRequest.Headers.Append(\"Content-MD5\", base64);\n        _inputRequest.Method = \"GET\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        ThenTheMappedRequestHasContentTypeHeader(\"application/json\");\r\n        Assert.NotNull(_mappedRequest.Content);\n        _mappedRequest.Content.Headers.ContentEncoding.ToArray()[0].ShouldBe(\"gzip\");\n        _mappedRequest.Content.Headers.ContentEncoding.ToArray()[1].ShouldBe(\"compress\");\n        Assert.NotNull(_mappedRequest.Content);\n        _mappedRequest.Content.Headers.ContentLanguage.First().ShouldBe(\"english\");\n        Assert.NotNull(_mappedRequest.Content);\n        Assert.NotNull(_mappedRequest.Content.Headers.ContentLocation);\n        _mappedRequest.Content.Headers.ContentLocation.OriginalString.ShouldBe(\"/my-receipts/38\");\n        Assert.NotNull(_mappedRequest.Content);\n        _mappedRequest.Content.Headers.ContentMD5.ShouldBe(md5Bytes);\n        Assert.NotNull(_mappedRequest.Content);\n        Assert.NotNull(_mappedRequest.Content.Headers.ContentRange);\n        _mappedRequest.Content.Headers.ContentRange.From.ShouldBe(1);\n        _mappedRequest.Content.Headers.ContentRange.To.ShouldBe(2);\n        Assert.NotNull(_mappedRequest.Content);\n        Assert.NotNull(_mappedRequest.Content.Headers.ContentDisposition);\n        _mappedRequest.Content.Headers.ContentDisposition.DispositionType.ShouldBe(\"inline\");\n\r\n        // Assert: Then The Content-* Headers Are Not Added To Non Content Headers\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-Disposition\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-ContentMD5\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-ContentRange\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-ContentLanguage\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-ContentEncoding\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-ContentLocation\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-Length\");\n        _mappedRequest.Headers.ShouldNotContain(x => x.Key == \"Content-Type\");\n    }\n\n    [Fact]\n    public void Should_not_add_content_headers()\n    {\n        // Arrange\r\n        GivenTheInputRequestHasContent(\"This is my content\");\r\n        _inputRequest.ContentType = \"application/json\";\r\n        _inputRequest.Method = \"POST\";\r\n        GivenTheInputRequestHasAValidUri();\r\n        GivenTheDownstreamRoute();\r\n\r\n        // Act\r\n        WhenMapped();\r\n\r\n        // Assert\r\n        ThenTheMappedRequestHasContentTypeHeader(\"application/json\");\r\n\r\n        // Assert: Then The Other Content Type Headers Are Not Mapped\n        Assert.NotNull(_mappedRequest.Content);\n        _mappedRequest.Content.Headers.ContentDisposition.ShouldBeNull();\n        _mappedRequest.Content.Headers.ContentMD5.ShouldBeNull();\n        _mappedRequest.Content.Headers.ContentRange.ShouldBeNull();\n        _mappedRequest.Content.Headers.ContentLanguage.ShouldBeEmpty();\n        _mappedRequest.Content.Headers.ContentEncoding.ShouldBeEmpty();\n        _mappedRequest.Content.Headers.ContentLocation.ShouldBeNull();\n    }\n\n    private void GivenTheDownstreamRoute()\n    {\n        _downstreamRoute = new DownstreamRouteBuilder()\n            .WithDownstreamHttpVersion(new Version(\"1.1\")).Build();\n    }\n\n    private void ThenTheMappedRequestHasContentTypeHeader(string expected)\n    {\n        Assert.NotNull(_mappedRequest.Content);\n        Assert.NotNull(_mappedRequest.Content.Headers.ContentType);\n        _mappedRequest.Content.Headers.ContentType.MediaType.ShouldBe(expected);\n    }\n\n    private void GivenTheInputRequestHasHost(string host)\n    {\n        _inputRequest.Host = new HostString(host);\n    }\n\n    private void GivenTheInputRequestHasPath(string path)\n    {\n        if (path != null)\n        {\n            _inputRequest.Path = path;\n        }\n    }\n\n    private void GivenTheInputRequestHasQueryString(string querystring)\n    {\n        if (querystring != null)\n        {\n            _inputRequest.QueryString = new QueryString(querystring);\n        }\n    }\n\n    private void GivenTheInputRequestHasAValidUri()\n    {\n        _inputRequest.Scheme = \"http\";\n        GivenTheInputRequestHasHost(\"www.google.com\");\n    }\n\n    private void GivenTheInputRequestHasContent(string content)\n    {\n        _inputRequest.ContentLength = content.Length;\n        _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(content));\n    }\n\n    private void GivenTheInputRequestHasChunkedContent(params string[] chunks)\n    {\n        // ASP.Net Core decodes chucked streams, so that the input request just sees the decoded data\n        // Because of that, we just give a stream with the concatenated chunks to the test\n        _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Join(\"\", chunks)));\n        _inputRequest.Headers.TransferEncoding = \"chunked\";\n    }\n\n    private void WhenMapped()\n    {\n        _mappedRequest = _requestMapper.Map(_inputRequest, _downstreamRoute);\n    }\n\n    private async Task ThenTheMappedRequestHasContent(string expectedContent)\n    {\n        Assert.NotNull(_mappedRequest.Content);\n        var contentAsString = await _mappedRequest.Content.ReadAsStringAsync();\r\n        contentAsString.ShouldBe(expectedContent);\n    }\n\n    private void ThenTheMappedRequestHasContentLength(long expectedLength)\n    {\n        Assert.NotNull(_mappedRequest.Content);\n        _mappedRequest.Content.Headers.ContentLength.ShouldBe(expectedLength);\n    }\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Request.Mapper;\nusing System.Reflection;\nusing System.Text;\n\nnamespace Ocelot.UnitTests.Request.Mapper;\n\npublic class StreamHttpContentTests\n{\n    private readonly DefaultHttpContext _httpContext;\n\n    private const string PayLoad =\n        \"[{\\\"_id\\\":\\\"65416ef7eafdf7953c4d7319\\\",\\\"index\\\":0,\\\"guid\\\":\\\"254b515d-0569-494d-9bc8-e21c8bd0365e\\\",\\\"isActive\\\":false,\\\"balance\\\":\\\"$1,225.59\\\",\\\"picture\\\":\\\"http://placehold.it/32x32\\\",\\\"age\\\":26,\\\"eyeColor\\\":\\\"blue\\\",\\\"name\\\":\\\"FayHatfield\\\",\\\"gender\\\":\\\"female\\\",\\\"company\\\":\\\"VIASIA\\\",\\\"email\\\":\\\"fayhatfield@viasia.com\\\",\\\"phone\\\":\\\"+1(970)416-2792\\\",\\\"address\\\":\\\"768MontroseAvenue,Mansfield,NewMexico,8890\\\",\\\"about\\\":\\\"Duisoccaecatdoloreeiusmoddoipsummollitaliquipnostrudqui.Cillumdoexercitationexercitationexcepteurincididuntadipisicingminimconsecteturofficiaanimdoloreincididuntlaborealiqua.Tempordoloreirurecillumadnullasuntoccaecatsitnulladosit.Sitnostrudullamcolaborisvelitvelitetofficiasitenimipsumaute.\\\\r\\\\n\\\",\\\"registered\\\":\\\"2023-07-03T03:10:08-02:00\\\",\\\"latitude\\\":0.117661,\\\"longitude\\\":-65.570177,\\\"tags\\\":[\\\"Lorem\\\",\\\"consequat\\\",\\\"consectetur\\\",\\\"pariatur\\\",\\\"fugiat\\\",\\\"est\\\",\\\"mollit\\\"],\\\"friends\\\":[{\\\"id\\\":0,\\\"name\\\":\\\"LynetteMelendez\\\"},{\\\"id\\\":1,\\\"name\\\":\\\"DrakeMay\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"JenningsConrad\\\"}],\\\"greeting\\\":\\\"Hello,FayHatfield!Youhave3unreadmessages.\\\",\\\"favoriteFruit\\\":\\\"apple\\\"}]\";\n\n    public StreamHttpContentTests()\n    {\n        _httpContext = new DefaultHttpContext();\n    }\n\n    [Fact]\n    public async Task Copy_body_to_stream_and_stream_content_should_match_payload()\n    {\n        // Arrange\n        var sut = StreamHttpContentFactory();\n        using var stream = new MemoryStream();\n\n        // Act\n        await sut.CopyToAsync(stream, TestContext.Current.CancellationToken);\n\n        // Assert\n        stream.Position = 0;\n        var result = Encoding.UTF8.GetString(stream.ToArray());\n        result.ShouldBe(PayLoad);\n    }\n\n    [Fact]\n    public async Task Copy_body_to_stream_with_unknown_length_and_stream_content_should_match_payload()\n    {\n        // Arrange\n        var bytes = Encoding.UTF8.GetBytes(PayLoad);\n        using var inputStream = new MemoryStream(bytes);\n        using var outputStream = new MemoryStream();\n\n        // Act\n        await CopyAsyncTest(\n            new StreamHttpContent(_httpContext),\n            new object[] { inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None });\n\n        // Assert\n        inputStream.Position = 0;\n        outputStream.Position = 0;\n        var result = Encoding.UTF8.GetString(outputStream.ToArray());\n        result.ShouldBe(PayLoad);\n    }\n\n    [Fact]\n    public async Task Copy_body_to_stream_with_body_length_and_stream_content_should_match_payload()\n    {\n        // Arrange\n        var bytes = Encoding.UTF8.GetBytes(PayLoad);\n        using var inputStream = new MemoryStream(bytes);\n        using var outputStream = new MemoryStream();\n\n        // Act\n        await CopyAsyncTest(\n            new StreamHttpContent(_httpContext),\n            new object[] { inputStream, outputStream, bytes.Length, false, CancellationToken.None });\n\n        // Assert\n        inputStream.Position = 0;\n        outputStream.Position = 0;\n        var result = Encoding.UTF8.GetString(outputStream.ToArray());\n        result.ShouldBe(PayLoad);\n    }\n\n    [Fact]\n    public async Task Should_throw_if_passed_body_length_does_not_match_real_body_length()\n    {\n        // Arrange\n        var bytes = Encoding.UTF8.GetBytes(PayLoad);\n        using var inputStream = new MemoryStream(bytes);\n        using var outputStream = new MemoryStream();\n\n        // Act, Assert\n        await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await CopyAsyncTest(\n                new StreamHttpContent(_httpContext),\n                new object[] { inputStream, outputStream, 10, false, CancellationToken.None }));\n    }\n\n    private StreamHttpContent StreamHttpContentFactory()\n    {\n        var bytes = Encoding.UTF8.GetBytes(PayLoad);\n        _httpContext.Request.Body = new MemoryStream(bytes);\n        return new StreamHttpContent(_httpContext);\n    }\n\n    private static async Task CopyAsyncTest(StreamHttpContent streamHttpContent, object[] parameters)\n    {\n        var bindingAttr = BindingFlags.NonPublic | BindingFlags.Static;\n        var method = typeof(StreamHttpContent).GetMethod(\"CopyAsync\", bindingAttr) ??\n            throw new Exception(\"Could not find CopyAsync\");\n        var task = (Task)method.Invoke(streamHttpContent, parameters) ??\n                   throw new Exception(\"Could not invoke CopyAsync\");\n        await task.ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.DownstreamRouteFinder;\nusing Ocelot.DownstreamRouteFinder.UrlMatcher;\nusing Ocelot.Infrastructure.RequestData;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.RequestId.Middleware;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.RequestId;\n\npublic class RequestIdMiddlewareTests : UnitTest\n{\n    private readonly HttpRequestMessage _downstreamRequest;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly RequestIdMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly Mock<IRequestScopedDataRepository> _repo;\n    private readonly DefaultHttpContext _httpContext;\n    public RequestIdMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, \"http://test.com\");\n        _repo = new Mock<IRequestScopedDataRepository>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<RequestIdMiddleware>()).Returns(_logger.Object);\n        _next = context =>\n        {\n            _httpContext.Response.Headers.Append(\"LSRequestId\", _httpContext.TraceIdentifier);\n            return Task.CompletedTask;\n        };\n        _middleware = new RequestIdMiddleware(_next, _loggerFactory.Object, _repo.Object);\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest));\n    }\n\n    [Fact]\n    public async Task Should_pass_down_request_id_from_upstream_request()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithRequestIdKey(\"LSRequestId\")\n                    .WithUpstreamHttpMethod([\"Get\"])\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        var requestId = Guid.NewGuid().ToString();\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenThereIsNoGlobalRequestId();\n        _httpContext.Request.Headers.TryAdd(\"LSRequestId\", requestId);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheTraceIdIs(requestId);\n    }\n\n    [Fact]\n    public async Task Should_add_request_id_when_not_on_upstream_request()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithRequestIdKey(\"LSRequestId\")\n                    .WithUpstreamHttpMethod([\"Get\"])\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenThereIsNoGlobalRequestId();\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert: Then The TraceId Is Anything\n        _httpContext.Response.Headers.TryGetValue(\"LSRequestId\", out var value);\n        value.First().ShouldNotBeNullOrEmpty();\n    }\n\n    [Fact]\n    public async Task Should_add_request_id_scoped_repo_for_logging_later()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithRequestIdKey(\"LSRequestId\")\n                    .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        var requestId = Guid.NewGuid().ToString();\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenThereIsNoGlobalRequestId();\n        _httpContext.Request.Headers.TryAdd(\"LSRequestId\", requestId);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheTraceIdIs(requestId);\n        _repo.Verify(x => x.Add(\"RequestId\", requestId), Times.Once);\n    }\n\n    [Fact]\n    public async Task Should_update_request_id_scoped_repo_for_logging_later()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithRequestIdKey(\"LSRequestId\")\n                    .WithUpstreamHttpMethod([\"Get\"])\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        var requestId = Guid.NewGuid().ToString();\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenTheRequestIdWasSetGlobally();\n        _httpContext.Request.Headers.TryAdd(\"LSRequestId\", requestId);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheTraceIdIs(requestId);\n        _repo.Verify(x => x.Update(\"RequestId\", requestId), Times.Once);\n    }\n\n    [Fact]\n    public async Task Should_not_update_if_global_request_id_is_same_as_re_route_request_id()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n                    .WithDownstreamPathTemplate(\"any old string\")\n                    .WithRequestIdKey(\"LSRequestId\")\n                    .WithUpstreamHttpMethod(new List<string> { \"Get\" })\n                    .Build();\n        var downstreamRoute = new DownstreamRouteHolder(\n            new List<PlaceholderNameAndValue>(),\n            new Route(route, HttpMethod.Get));\n\n        var requestId = \"alreadyset\";\n\n        GivenTheDownStreamRouteIs(downstreamRoute);\n        GivenTheRequestIdWasSetGlobally();\n        _httpContext.Request.Headers.TryAdd(\"LSRequestId\", requestId);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        ThenTheTraceIdIs(requestId);\n        _repo.Verify(x => x.Update(\"RequestId\", requestId), Times.Never);\n    }\n\n    private void GivenThereIsNoGlobalRequestId()\n    {\n        _repo.Setup(x => x.Get<string>(\"RequestId\")).Returns(new OkResponse<string>(null));\n    }\n\n    private void GivenTheRequestIdWasSetGlobally()\n    {\n        _repo.Setup(x => x.Get<string>(\"RequestId\")).Returns(new OkResponse<string>(\"alreadyset\"));\n    }\n\n    private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute)\n    {\n        _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues);\n        _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]);\n    }\n\n    private void ThenTheTraceIdIs(string expected)\n    {\n        _httpContext.Response.Headers.TryGetValue(\"LSRequestId\", out var value);\n        value.First().ShouldBe(expected);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/DelegatingHandlerFactoryTests.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing Ocelot.QualityOfService;\nusing Ocelot.Requester;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\nnamespace Ocelot.UnitTests.Requester;\n\npublic class DelegatingHandlerFactoryTests : UnitTest\n{\n    private DelegatingHandlerFactory _factory;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IQoSFactory> _qosFactory;\n    private readonly Mock<ITracingHandlerFactory> _tracingFactory;\n    private readonly Mock<IOptionsMonitor<FileConfiguration>> _optionsMonitor;\n    private IServiceProvider _serviceProvider;\n    private readonly IServiceCollection _services;\n    private readonly QosDelegatingHandlerDelegate _qosDelegate;\n\n    public DelegatingHandlerFactoryTests()\n    {\n        _qosDelegate = (a, b, c) => new FakeQoSHandler();\n        _tracingFactory = new Mock<ITracingHandlerFactory>();\n        _qosFactory = new Mock<IQoSFactory>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<DelegatingHandlerFactory>()).Returns(_logger.Object);\n        _services = new ServiceCollection();\n        _services.AddSingleton(_qosDelegate);\n        _optionsMonitor = new();\n        _optionsMonitor.SetupGet(x => x.CurrentValue).Returns(new FileConfiguration());\n        _services.AddSingleton(_optionsMonitor.Object);\n    }\n\n    protected static FileHttpHandlerOptions GivenHandlerOptions => new()\n    {\n        AllowAutoRedirect = true,\n        UseCookieContainer = true,\n        UseProxy = true,\n        UseTracing = true,\n    };\n\n    private static QoSOptions GivenQoS() => new(1, 1)\n    {\n        Timeout = 1,\n    };\n\n    [Fact]\n    public void Should_follow_ordering_add_specifics()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithDelegatingHandlers(new List<string>\n            {\n                \"FakeDelegatingHandler\",\n                \"FakeDelegatingHandlerTwo\",\n            })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandlerThree, FakeDelegatingHandlerFour>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(6);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerThree>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerFour>(1);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(2);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(3);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(4);\n        result.ThenHandlerAtPositionIs<FakeQoSHandler>(5);\n    }\n\n    [Fact]\n    public void Should_follow_ordering_order_specifics_and_globals()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithDelegatingHandlers(new List<string>\n            {\n                \"FakeDelegatingHandlerTwo\",\n                \"FakeDelegatingHandler\",\n                \"FakeDelegatingHandlerFour\",\n            })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandlerFour, FakeDelegatingHandlerThree>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(6);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerThree>(0); //first because global not in config\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(1); //first from config\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(2); //second from config\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerFour>(3); //third from config (global)\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(4);\n        result.ThenHandlerAtPositionIs<FakeQoSHandler>(5);\n    }\n\n    [Fact]\n    public void Should_follow_ordering_order_specifics()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithDelegatingHandlers(new List<string>\n            {\n                \"FakeDelegatingHandlerTwo\",\n                \"FakeDelegatingHandler\",\n            })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandlerThree, FakeDelegatingHandlerFour>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(6);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerThree>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerFour>(1);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(2);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(3);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(4);\n        result.ThenHandlerAtPositionIs<FakeQoSHandler>(5);\n    }\n\n    [Fact]\n    public void Should_follow_ordering_order_and_only_add_specifics_in_config()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithDelegatingHandlers(new List<string>\n            {\n                \"FakeDelegatingHandler\",\n            })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandlerThree, FakeDelegatingHandlerFour>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(5);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerThree>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerFour>(1);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(2);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(3);\n        result.ThenHandlerAtPositionIs<FakeQoSHandler>(4);\n    }\n\n    [Fact]\n    public void Should_follow_ordering_dont_add_specifics()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(4);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(1);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(2);\n        result.ThenHandlerAtPositionIs<FakeQoSHandler>(3);\n    }\n\n    [Fact]\n    public void Should_apply_re_route_specific()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(new())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false })\n            .WithDelegatingHandlers(new List<string>\n            {\n                \"FakeDelegatingHandler\",\n                \"FakeDelegatingHandlerTwo\",\n            })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(2);\n        result.ThenTheDelegatesAreAddedCorrectly();\n    }\n\n    [Fact]\n    public void Should_all_from_all_routes_provider_and_qos()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(3);\n        result.ThenTheDelegatesAreAddedCorrectly();\n        result.ThenItIsQosHandler(2);\n    }\n\n    [Fact]\n    public void Should_return_provider_with_no_delegates()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(new())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheServiceProviderReturnsNothing();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert: Then No Delegates Are In The Provider\n        result.ShouldNotBeNull().ShouldBeEmpty();\n    }\n\n    [Fact]\n    public void Should_return_provider_with_qos_delegate()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheServiceProviderReturnsNothing();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(1);\n        result.ThenItIsQosHandler(0);\n    }\n\n    [Fact]\n    public void Should_return_provider_with_qos_delegate_when_timeout_value_set()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(new(timeout: 1))\n            .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false })\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        GivenTheQosFactoryReturns(new FakeQoSHandler());\n        GivenTheServiceProviderReturnsNothing();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(1);\n        result.ThenItIsQosHandler(0);\n    }\n\n    [Fact]\n    public void Should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_error()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        _qosFactory.Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(new ErrorResponse<DelegatingHandler>(new AnyError()));\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(4);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(1);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(2);\n        result.ThenHandlerAtPositionIs<NoQosDelegatingHandler>(3);\n        ThenTheWarningIsLogged(route);\n    }\n\n    [Fact]\n    public void Should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_null()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(GivenQoS())\n            .WithHttpHandlerOptions(new(GivenHandlerOptions))\n            .WithLoadBalancerKey(string.Empty)\n            .Build();\n        _qosFactory.Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns((ErrorResponse<DelegatingHandler>)null);\n        GivenTheTracingFactoryReturns();\n        GivenTheServiceProviderReturnsGlobalDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n        GivenTheServiceProviderReturnsSpecificDelegatingHandlers<FakeDelegatingHandler, FakeDelegatingHandlerTwo>();\n\n        // Act\n        var result = WhenIGet(route);\n\n        // Assert\n        result.ThenThereIsDelegatesInProvider(4);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandler>(0);\n        result.ThenHandlerAtPositionIs<FakeDelegatingHandlerTwo>(1);\n        result.ThenHandlerAtPositionIs<FakeTracingHandler>(2);\n        result.ThenHandlerAtPositionIs<NoQosDelegatingHandler>(3);\n        ThenTheWarningIsLogged(route);\n    }\n\n    private void ThenTheWarningIsLogged(DownstreamRoute route)\n    {\n        _logger.Verify(x => x.LogWarning(It.Is<Func<string>>(\n            y => y.Invoke() == $\"Route '{route.Name()}' specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!\")),\n            Times.Once);\n    }\n\n    private void GivenTheTracingFactoryReturns()\n    {\n        _tracingFactory\n            .Setup(x => x.Get())\n            .Returns(new FakeTracingHandler());\n    }\n\n    private void GivenTheServiceProviderReturnsGlobalDelegatingHandlers<TOne, TTwo>()\n        where TOne : DelegatingHandler\n        where TTwo : DelegatingHandler\n    {\n        _services.AddTransient<TOne>();\n        _services.AddTransient(s =>\n        {\n            var service = s.GetService<TOne>();\n            return new GlobalDelegatingHandler(service);\n        });\n        _services.AddTransient<TTwo>();\n        _services.AddTransient(s =>\n        {\n            var service = s.GetService<TTwo>();\n            return new GlobalDelegatingHandler(service);\n        });\n    }\n\n    private void GivenTheServiceProviderReturnsSpecificDelegatingHandlers<TOne, TTwo>()\n        where TOne : DelegatingHandler\n        where TTwo : DelegatingHandler\n    {\n        _services.AddTransient<DelegatingHandler, TOne>();\n        _services.AddTransient<DelegatingHandler, TTwo>();\n    }\n\n    private void GivenTheServiceProviderReturnsNothing()\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n    }\n\n    private void GivenTheQosFactoryReturns(DelegatingHandler handler)\n    {\n        _qosFactory\n            .Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(new OkResponse<DelegatingHandler>(handler));\n    }\n\n    private List<DelegatingHandler> WhenIGet(DownstreamRoute route)\n    {\n        _serviceProvider = _services.BuildServiceProvider(true);\n        _factory = new DelegatingHandlerFactory(_tracingFactory.Object, _qosFactory.Object, _serviceProvider, _loggerFactory.Object);\n        return _factory.Get(route);\n    }\n}\n\ninternal static class ListExtensions\n{\n    public static void ThenItIsQosHandler(this List<DelegatingHandler> result, int i)\n    {\n        result[i].ShouldNotBeNull().ShouldBeOfType<FakeQoSHandler>();\n    }\n\n    public static void ThenTheDelegatesAreAddedCorrectly(this List<DelegatingHandler> result)\n    {\n        var handler = (FakeDelegatingHandler)result[0].ShouldNotBeNull();\n        handler.Order.ShouldBe(1);\n\n        var handlerTwo = (FakeDelegatingHandlerTwo)result[1].ShouldNotBeNull();\n        handlerTwo.Order.ShouldBe(2);\n    }\n\n    public static void ThenThereIsDelegatesInProvider(this List<DelegatingHandler> result, int count)\n    {\n        result.ShouldNotBeNull().Count.ShouldBe(count);\n    }\n\n    public static void ThenHandlerAtPositionIs<T>(this List<DelegatingHandler> result, int pos)\n        where T : DelegatingHandler\n    {\n        result[pos].ShouldNotBeNull().ShouldBeOfType<T>();\n    }\n}\n\ninternal class FakeTracingHandler : DelegatingHandler, ITracingHandler\n{\n}\n\ninternal class FakeQoSHandler : DelegatingHandler\n{\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs",
    "content": "namespace Ocelot.UnitTests.Requester;\n\npublic class FakeDelegatingHandler : DelegatingHandler\n{\n    public FakeDelegatingHandler() => Order = 1;\n    public FakeDelegatingHandler(int order) => Order = order;\n    public int Order { get; }\n    public DateTime TimeCalled { get; private set; }\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        TimeCalled = DateTime.Now;\n        return Task.FromResult(new HttpResponseMessage());\n    }\n}\n\npublic class FakeDelegatingHandlerThree : DelegatingHandler\n{\n    public FakeDelegatingHandlerThree() => Order = 3;\n    public int Order { get; }\n    public DateTime TimeCalled { get; private set; }\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        TimeCalled = DateTime.Now;\n        return Task.FromResult(new HttpResponseMessage());\n    }\n}\n\npublic class FakeDelegatingHandlerFour : DelegatingHandler\n{\n    public FakeDelegatingHandlerFour() => Order = 4;\n    public int Order { get; }\n    public DateTime TimeCalled { get; private set; }\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        TimeCalled = DateTime.Now;\n        return Task.FromResult(new HttpResponseMessage());\n    }\n}\n\npublic class FakeDelegatingHandlerTwo : DelegatingHandler\n{\n    public FakeDelegatingHandlerTwo() => Order = 2;\n    public int Order { get; }\n    public DateTime TimeCalled { get; private set; }\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        TimeCalled = DateTime.Now;\n        return Task.FromResult(new HttpResponseMessage());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Errors;\nusing Ocelot.Request.Mapper;\nusing Ocelot.Requester;\nusing Ocelot.UnitTests.Responder;\n\nnamespace Ocelot.UnitTests.Requester;\n\n[Trait(\"Feat\", \"562\")] // https://github.com/ThreeMammals/Ocelot/pull/562\n[Trait(\"Release\", \"10.0.3\")] // https://github.com/ThreeMammals/Ocelot/releases/tag/10.0.3\npublic class HttpExceptionToErrorMapperTests\n{\n    private HttpExceptionToErrorMapper _mapper;\n    private readonly ServiceCollection _services;\n\n    public HttpExceptionToErrorMapperTests()\n    {\n        _services = new ServiceCollection();\n        var provider = _services.BuildServiceProvider(true);\n        _mapper = new HttpExceptionToErrorMapper(provider);\n    }\n\n    [Fact]\n    public void Should_return_default_error_because_mappers_are_null()\n    {\n        // Arrange, Act\n        var error = _mapper.Map(new Exception());\n\n        // Assert\n        error.ShouldBeOfType<UnableToCompleteRequestError>();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"902\")] // https://github.com/ThreeMammals/Ocelot/pull/902\n    public void Should_return_request_canceled()\n    {\n        // Arrange, Act\n        var error = _mapper.Map(new OperationCanceledException());\n\n        // Assert\n        error.ShouldBeOfType<RequestCanceledError>();\n    }\n\n    [Fact]\n    public void Should_return_ConnectionToDownstreamServiceError()\n    {\n        // Arrange, Act\n        var error = _mapper.Map(new HttpRequestException());\n\n        // Assert\n        error.ShouldBeOfType<ConnectionToDownstreamServiceError>();\n    }\n\n    private class SomeException : OperationCanceledException { }\n\n    [Fact]\n    public void Should_return_request_canceled_for_subtype()\n    {\n        // Arrange, Act\n        var error = _mapper.Map(new SomeException());\n\n        // Assert\n        error.ShouldBeOfType<RequestCanceledError>();\n    }\n\n    [Fact]\n    public void Should_return_error_from_mapper()\n    {\n        // Arrange\n        IDictionary<Type, Func<Exception, Error>> errorMapping = new Dictionary<Type, Func<Exception, Error>>\n        {\n            {typeof(TaskCanceledException), e => new AnyError()},\n        };\n\n        _services.AddSingleton(errorMapping);\n\n        var provider = _services.BuildServiceProvider(true);\n\n        _mapper = new HttpExceptionToErrorMapper(provider);\n\n        // Act\n        var error = _mapper.Map(new TaskCanceledException());\n\n        // Assert\n        error.ShouldBeOfType<AnyError>();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1824\")] // https://github.com/ThreeMammals/Ocelot/pull/1824\n    public void Map_TimeoutException_To_RequestTimedOutError()\n    {\n        // Arrange\n        var ex = new TimeoutException(\"test\");\n\n        // Act\n        var error = _mapper.Map(ex);\n\n        // Assert\n        Assert.IsType<RequestTimedOutError>(error);\n        Assert.Equal(25, (int)error.Code);\n        Assert.Equal(503, error.HttpStatusCode);\n        Assert.Equal(\"Timeout making http request, exception: System.TimeoutException: test\", error.Message);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"749\")] // https://github.com/ThreeMammals/Ocelot/issues/749\n    [Trait(\"PR\", \"1769\")] // https://github.com/ThreeMammals/Ocelot/pull/1769\n    public void Map_BadHttpRequestException_To_PayloadTooLargeError()\n    {\n        // Arrange\n        var inner = new BadHttpRequestException(\"test-inner\", 413);\n        var ex = new HttpRequestException(\"test\", inner);\n\n        // Act\n        var error = _mapper.Map(ex);\n\n        // Assert\n        Assert.IsType<PayloadTooLargeError>(error);\n        Assert.Equal(41, (int)error.Code);\n        Assert.Equal(413, error.HttpStatusCode);\n        Assert.Equal(\"test\", error.Message);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Requester;\nusing Ocelot.Requester.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.UnitTests.Responder;\n\r\nnamespace Ocelot.UnitTests.Requester;\r\n\r\npublic class HttpRequesterMiddlewareTests : UnitTest\r\n{\r\n    private readonly Mock<IHttpRequester> _requester;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n    private readonly HttpRequesterMiddleware _middleware;\r\n    private readonly RequestDelegate _next;\r\n    private readonly DefaultHttpContext _httpContext;\r\n\r\n    public HttpRequesterMiddlewareTests()\r\n    {\r\n        _httpContext = new DefaultHttpContext();\r\n        _requester = new Mock<IHttpRequester>();\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _loggerFactory.Setup(x => x.CreateLogger<HttpRequesterMiddleware>()).Returns(_logger.Object);\r\n        _next = context => Task.CompletedTask;\r\n        _middleware = new HttpRequesterMiddleware(_next, _loggerFactory.Object, _requester.Object);\r\n\r\n        _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build()); // Given The Request Is\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_call_services_correctly()\r\n    {\r\n        // Arrange\r\n        var response = GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.OK)));\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert\r\n        InformationIsLogged();\r\n\r\n        // Assert: Then The Downstream Response Is Set\r\n        foreach (var httpResponseHeader in response.Data.Headers)\r\n        {\r\n            if (_httpContext.Items.DownstreamResponse().Headers.Any(x => x.Key == httpResponseHeader.Key))\r\n            {\r\n                throw new Exception(\"Header in response not in downstreamresponse headers\");\r\n            }\r\n        }\r\n\r\n        _httpContext.Items.DownstreamResponse().Content.ShouldBe(response.Data.Content);\r\n        _httpContext.Items.DownstreamResponse().StatusCode.ShouldBe(response.Data.StatusCode);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_set_error()\r\n    {\r\n        // Arrange\r\n        GivenTheRequesterReturns(new ErrorResponse<HttpResponseMessage>(new AnyError()));\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert\r\n        _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_log_downstream_internal_server_error()\r\n    {\r\n        // Arrange\r\n        GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError)));\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert\r\n        WarningIsLogged();\r\n    }\r\n\n    [Theory]\n    [Trait(\"Bug\", \"1953\")]\n    [InlineData(HttpStatusCode.OK)]\r\n    [InlineData(HttpStatusCode.PermanentRedirect)]\r\n    public async Task Should_LogInformation_when_status_is_less_than_BadRequest(HttpStatusCode status)\r\n    {\r\n        // Arrange\r\n        GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage(status)));\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert\r\n        InformationIsLogged();\r\n    }\r\n\r\n    [Theory]\n    [Trait(\"Bug\", \"1953\")]\n    [InlineData(HttpStatusCode.BadRequest)]\r\n    [InlineData(HttpStatusCode.NotFound)]\r\n    public async Task Should_LogWarning_when_status_is_BadRequest_or_greater(HttpStatusCode status)\r\n    {\r\n        // Arrange\r\n        GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage(status)));\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert\r\n        WarningIsLogged();\r\n    }\r\n\r\n    private Response<HttpResponseMessage> GivenTheRequesterReturns(Response<HttpResponseMessage> response)\r\n    {\r\n        _requester.Setup(x => x.GetResponse(It.IsAny<HttpContext>()))\r\n            .ReturnsAsync(response);\r\n        return response;\r\n    }\r\n\r\n    private void WarningIsLogged()\r\n    {\r\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()),\r\n            Times.Once);\r\n    }\r\n\r\n    private void InformationIsLogged()\r\n    {\r\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()),\r\n            Times.Once);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs",
    "content": "﻿using Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing static Ocelot.Requester.MessageInvokerPool;\n\nnamespace Ocelot.UnitTests.Requester;\n\npublic class MessageInvokerCacheKeyTests\n{\n    [Fact]\n    public void Equals_Object()\n    {\n        var route1 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r1\", 0, false, \"/r1\"))\n            .Build();\n        var route2 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r2\", 0, false, \"/r2\"))\n            .Build();\n        var key1 = new MessageInvokerCacheKey(route1);\n        object key2 = new MessageInvokerCacheKey(route2);\n\n        // Act, Assert 0: If different types\n        bool isDiffTypes = key1.Equals(new DownstreamRouteBuilder());\n        Assert.False(isDiffTypes);\n\n        // Act, Assert 1\n        bool isEqual = key1.Equals(key2);\n        Assert.False(isEqual);\n\n        // Arrange 2\n        var route3 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r1\", 0, false, \"/r3\"))\n            .Build();\n        object key3 = new MessageInvokerCacheKey(route3);\n\n        // Act, Assert 1\n        isEqual = key1.Equals(key3);\n        Assert.False(isEqual);\n\n        // Arrange 3\n        var route4 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r1\", 0, false, \"/r1\"))\n            .Build();\n        object key4 = new MessageInvokerCacheKey(route4);\n\n        // Act, Assert 1\n        isEqual = key1.Equals(key4);\n\n        // Assert.True(isEqual); // O-ho-ho! :(\n        Assert.False(isEqual); // actually objects are different :(\n\n        // Life hack for Guillaume ;)) LoL\n        // This method has taken from source code of the public sealed partial class ObjectEqualityComparer<T> : EqualityComparer<T>\n        // Link to source: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs#L186-L198\n        static bool EqualsForGui(DownstreamRoute x, DownstreamRoute y)\n        {\n            if (x != null)\n            {\n                if (y != null) return x.Equals(y); // object.Equals(object) -> https://github.com/dotnet/runtime/blob/0621e649bd084cb0dfd1f2e627538e7d9aa9e211/src/libraries/System.Private.CoreLib/src/System/Object.cs#L45-L64\n                return false;\n            }\n\n            if (y != null) return false;\n            return true;\n        }\n\n        // Two object with absolutely identical internal state\n        var d1 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default);\n        var d2 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default);\n\n        // Act for Gui :) This will be a gift for Gui for Christmas! LOL\n        bool happyStart = d1.Equals(d2); // object.Equals(object)\n        bool happyEnd = EqualsForGui(d1, d2); // also uses object.Equals(object)\n\n        // Assert Bingo!\n        Assert.Equal(happyStart, happyEnd);\n        Assert.True(EqualsForGui(d1, d1)); // it is true because same reference was compared to itself by object.Equals(object), so this is default implementation of object.Equals(object)\n\n        // Assert.True(happyEnd); // but it is False actually \n        Assert.False(happyEnd); // No happy end by Gui...\n    }\n\n    [Fact]\n    public void Equality_Operator()\n    {\n        var route1 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r1\", 0, false, \"/r1\"))\n            .Build();\n        var route2 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r2\", 0, false, \"/r2\"))\n            .Build();\n        var key1 = new MessageInvokerCacheKey(route1);\n        var key2 = new MessageInvokerCacheKey(route2);\n\n        // Act\n        bool isEqual = key1 == key2;\n\n        // Assert\n        Assert.False(isEqual); \n    }\n\n    [Fact]\n    public void Inequality_Operator()\n    {\n        var route1 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r1\", 0, false, \"/r1\"))\n            .Build();\n        var route2 = new DownstreamRouteBuilder()\n            .WithUpstreamPathTemplate(new(\"/r2\", 0, false, \"/r2\"))\n            .Build();\n        var key1 = new MessageInvokerCacheKey(route1);\n        var key2 = new MessageInvokerCacheKey(route2);\n\n        // Act\n        bool notEqual = key1 != key2;\n\n        // Assert\n        Assert.True(notEqual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/MessageInvokerHttpRequesterTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Errors;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Requester;\nusing Ocelot.Responses;\n\nnamespace Ocelot.UnitTests.Requester;\n\npublic class MessageInvokerHttpRequesterTests\n{\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactoryMock;\n    private readonly Mock<IOcelotLogger> _loggerMock;\n    private readonly Mock<IMessageInvokerPool> _messageInvokerPoolMock;\n    private readonly Mock<IExceptionToErrorMapper> _mapperMock;\n    private readonly Mock<HttpMessageInvoker> _messageInvokerMock;\n    private readonly MessageInvokerHttpRequester _sut;\n\n    public MessageInvokerHttpRequesterTests()\n    {\n        _loggerFactoryMock = new Mock<IOcelotLoggerFactory>();\n        _loggerMock = new Mock<IOcelotLogger>();\n        _loggerFactoryMock.Setup(f => f.CreateLogger<MessageInvokerHttpRequester>())\n            .Returns(_loggerMock.Object);\n\n        _messageInvokerPoolMock = new Mock<IMessageInvokerPool>();\n        _mapperMock = new Mock<IExceptionToErrorMapper>();\n        _messageInvokerMock = new Mock<HttpMessageInvoker>(new HttpClientHandler());\n\n        _sut = new MessageInvokerHttpRequester(\n            _loggerFactoryMock.Object,\n            _messageInvokerPoolMock.Object,\n            _mapperMock.Object);\n    }\n\n    private static DefaultHttpContext CreateHttpContext()\n    {\n        var context = new DefaultHttpContext();\n        // Ocelot adds DownstreamRequest and DownstreamRoute into Items\n        var downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test\"));\n        var downstreamRoute = new DownstreamRouteBuilder().Build();\n\n        context.Items.UpsertDownstreamRequest(downstreamRequest);\n        context.Items.UpsertDownstreamRoute(downstreamRoute);\n        return context;\n    }\n\n    [Fact]\n    public async Task GetResponse_ReturnsOkResponse_WhenMessageInvokerSucceeds()\n    {\n        // Arrange\n        var context = CreateHttpContext();\n        var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK);\n\n        _messageInvokerPoolMock\n            .Setup(p => p.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(_messageInvokerMock.Object);\n\n        _messageInvokerMock\n            .Setup(m => m.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(expectedResponse);\n\n        // Act\n        var result = await _sut.GetResponse(context);\n\n        // Assert\n        var okResponse = Assert.IsType<OkResponse<HttpResponseMessage>>(result);\n        Assert.Equal(expectedResponse, okResponse.Data);\n    }\n\n    [Fact]\n    public async Task GetResponse_ReturnsErrorResponse_WhenMessageInvokerThrows()\n    {\n        // Arrange\n        var context = CreateHttpContext();\n        var exception = new InvalidOperationException(\"Test exception\");\n        var expectedError = new UnableToCompleteRequestError(new(\"mapped-error\"));\n\n        _messageInvokerPoolMock\n            .Setup(p => p.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(_messageInvokerMock.Object);\n\n        _messageInvokerMock\n            .Setup(m => m.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .ThrowsAsync(exception);\n\n        _mapperMock.Setup(m => m.Map(exception)).Returns(expectedError);\n\n        // Act\n        var result = await _sut.GetResponse(context);\n\n        // Assert\n        var errorResponse = Assert.IsType<ErrorResponse<HttpResponseMessage>>(result);\n        Assert.Contains(expectedError, errorResponse.Errors);\n    }\n\n    [Fact]\n    public void Constructor_CreatesInstance_WhenDependenciesAreValid()\n    {\n        // Arrange\n        var loggerFactoryMock = new Mock<IOcelotLoggerFactory>();\n        var loggerMock = new Mock<IOcelotLogger>();\n        loggerFactoryMock.Setup(f => f.CreateLogger<MessageInvokerHttpRequester>())\n            .Returns(loggerMock.Object);\n\n        var messageInvokerPoolMock = new Mock<IMessageInvokerPool>();\n        var mapperMock = new Mock<IExceptionToErrorMapper>();\n\n        // Act\n        var sut = new MessageInvokerHttpRequester(\n            loggerFactoryMock.Object,\n            messageInvokerPoolMock.Object,\n            mapperMock.Object);\n\n        // Assert\n        Assert.NotNull(sut);\n        // Verify that the logger factory was used to create a logger\n        loggerFactoryMock.Verify(f => f.CreateLogger<MessageInvokerHttpRequester>(), Times.Once);\n    }\n\n    [Fact]\n    public void Constructor_ThrowsArgumentNullException_WhenLoggerFactoryIsNull()\n    {\n        // Arrange\n        var messageInvokerPoolMock = new Mock<IMessageInvokerPool>();\n        var mapperMock = new Mock<IExceptionToErrorMapper>();\n\n        // Act & Assert\n        Assert.Throws<ArgumentNullException>(() =>\n            new MessageInvokerHttpRequester(null!, messageInvokerPoolMock.Object, mapperMock.Object));\n    }\n\n    [Fact]\n    public void Constructor_ThrowsArgumentNullException_WhenMessageInvokerPoolIsNull()\n    {\n        // Arrange\n        var loggerFactoryMock = new Mock<IOcelotLoggerFactory>();\n        var mapperMock = new Mock<IExceptionToErrorMapper>();\n\n        // Act & Assert\n        Assert.Throws<ArgumentNullException>(() =>\n            new MessageInvokerHttpRequester(loggerFactoryMock.Object, null!, mapperMock.Object));\n    }\n\n    [Fact]\n    public void Constructor_ThrowsArgumentNullException_WhenMapperIsNull()\n    {\n        // Arrange\n        var loggerFactoryMock = new Mock<IOcelotLoggerFactory>();\n        var messageInvokerPoolMock = new Mock<IMessageInvokerPool>();\n\n        // Act & Assert\n        Assert.Throws<ArgumentNullException>(() =>\n            new MessageInvokerHttpRequester(loggerFactoryMock.Object, messageInvokerPoolMock.Object, null!));\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.File;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Requester;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing System.Net.Security;\nusing System.Reflection;\nusing Xunit.Sdk;\n\nnamespace Ocelot.UnitTests.Requester;\n\npublic class MessageInvokerPoolTests : MessageInvokerPoolBase\n{\n    private DownstreamRoute _downstreamRoute1;\n    private DownstreamRoute _downstreamRoute2;\n\n    [Fact]\n    [Trait(\"PR\", \"1824\")]\n    public void If_calling_the_same_downstream_route_twice_should_return_the_same_message_invoker()\n    {\n        // Arrange\n        _downstreamRoute1 = DownstreamRouteFactory(\"/super-test\");\n        AndAHandlerFactory();\n        GivenAMessageInvokerPool();\n\n        // Act\n        var firstInvoker = _pool.Get(_downstreamRoute1);\n        var secondInvoker = _pool.Get(_downstreamRoute1);\n\n        // Assert\n        Assert.Equal(firstInvoker, secondInvoker);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1824\")]\n    public void If_calling_two_different_downstream_routes_should_return_different_message_invokers()\n    {\n        // Arrange\n        _downstreamRoute1 = DownstreamRouteFactory(\"/super-test\");\n        _downstreamRoute2 = DownstreamRouteFactory(\"/super-test\");\n        AndAHandlerFactory();\n        GivenAMessageInvokerPool();\n\n        // Act\n        var firstInvoker = _pool.Get(_downstreamRoute1);\n        var secondInvoker = _pool.Get(_downstreamRoute2);\n\n        // Assert\n        Assert.NotEqual(firstInvoker, secondInvoker);\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1824\")]\n    public async Task If_two_delegating_handlers_are_defined_then_these_should_be_call_in_order()\n    {\n        // Arrange\n        var fakeOne = new FakeDelegatingHandler();\n        var fakeTwo = new FakeDelegatingHandler();\n        var handlers = new List<DelegatingHandler> { fakeOne, fakeTwo };\n        GivenTheFactoryReturns(handlers);\n        _downstreamRoute1 = DownstreamRouteFactory(\"/super-test\");\n        GivenAMessageInvokerPool();\n        var port = PortFinder.GetRandomPort();\n        GivenARequestWithAUrlAndMethod(_downstreamRoute1, $\"http://localhost:{port}\", HttpMethod.Get);\n\n        // Act\n        await WhenICallTheClient(\"http://www.bbc.co.uk\");\n\n        // Assert\n        ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo);\n        _response.ShouldNotBeNull();\n    }\n\n    [Fact]\n    [Trait(\"PR\", \"1824\")]\n    public async Task Should_log_if_ignoring_ssl_errors()\n    {\n        // Arrange\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(new())\n            .WithHttpHandlerOptions(new() { UseProxy = true })\n            .WithLoadBalancerKey(string.Empty)\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build())\n            .WithDangerousAcceptAnyServerCertificateValidator(true)\n\n            // The test should pass without timeout definition -> implicit default timeout\n            //.WithTimeout(DownstreamRoute.DefaultTimeoutSeconds)\n            .Build();\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        var port = PortFinder.GetRandomPort();\n        GivenARequest(route, port);\n\n        // Act\n        await WhenICallTheClient(\"http://www.google.com/\");\n\n        // Assert: Then the DangerousAcceptAnyServerCertificateValidator warning is logged\n        _ocelotLogger.Verify(\n            x => x.LogWarning(It.Is<Func<string>>(y => y.Invoke() == $\"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute -> {_context.Items.DownstreamRoute().Name()}\")),\n            Times.Once);\n    }\n\n    #region PR 2073\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(1)]\n    [InlineData(3)]\n    public void SendAsync_NoQosAndHasRouteTimeout_ThrowTimeoutExceptionAfterRouteTimeout(int routeTimeoutSeconds)\n    {\n        // Arrange\n        var route = GivenRoute(null, routeTimeoutSeconds);\n        GivenTheFactoryReturnsNothing();\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        var port = PortFinder.GetRandomPort();\n        GivenARequest(route, port);\n\n        //// Act, Assert\n        //int marginMs = 50;\n        //var expected = TimeSpan.FromSeconds(routeTimeoutSeconds);\n        //var watcher = await TestRetry.NoWaitAsync(\n        //    () => WhenICallTheClientWillThrowAfterTimeout(expected, marginMs *= 2)); // call up to 3 times with margins 100, 200, 400\n        //AssertTimeoutPrecisely(watcher, expected);\n        // Act\n        using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n        // Assert\n        AssertTimeout(invoker, routeTimeoutSeconds);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(1, 2)]\n    [InlineData(3, 4)]\n    public void CreateMessageInvoker_QosTimeoutAndRouteOne_CreatedTimeoutDelegatingHandlerWithoutQosTimeout(int qosTimeout, int routeTimeout)\n    {\n        // Arrange\n        var route = GivenRoute(qosTimeout, routeTimeout);\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        GivenARequest(route, PortFinder.GetRandomPort());\n\n        // Act\n        using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n        // Assert\n        var actual = AssertTimeout(invoker, routeTimeout);\n        Assert.NotEqual(qosTimeout, (int)actual.TotalSeconds);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(1, 2, 2, 0, \"\")] // QoS timeout < route timeout\n    [InlineData(3, 4, 4, 0, \"\")] // QoS timeout < route timeout\n    [InlineData(2, 1, 4, 1, \"Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (1000 ms) is shorter than the QoS Timeout (2000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 4000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\")] // QoS timeout > route timeout\n    [InlineData(4, 3, 8, 1, \"Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (3000 ms) is shorter than the QoS Timeout (4000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 8000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\")] // QoS timeout > route timeout\n    [InlineData(5, 5, 10, 1, \"Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (5000 ms) is equal to the QoS Timeout (5000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 10000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\")] // QoS timeout == route timeout\n    [InlineData(DownstreamRoute.DefTimeout + 1, null, 2 * (DownstreamRoute.DefTimeout + 1), 1, \"Route '/' has Quality of Service settings (QoSOptions) enabled, but either the DownstreamRoute.DefaultTimeoutSeconds or the QoS Timeout is misconfigured: specifically, the DownstreamRoute.DefaultTimeoutSeconds (90000 ms) is shorter than the QoS Timeout (91000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 182000 ms to the route Timeout instead of using DownstreamRoute.DefaultTimeoutSeconds. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\")] // DefaultTimeoutSeconds as route timeout\n    public void EnsureRouteTimeoutIsGreaterThanQosOne_QosTimeoutVsRouteOne_ExpectedRouteTimeoutOrDoubledQosTimeout(int qosTimeout, int? routeTimeout, int expectedSeconds, int loggedCount, string expectedMessage)\n    {\n        // Arrange\n        var route = GivenRoute(qosTimeout, routeTimeout);\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        GivenARequest(route, PortFinder.GetRandomPort());\n        Func<string> fMsg = null;\n        _ocelotLogger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => fMsg = f);\n\n        // Act\n        using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n        // Assert\n        Assert.NotEqual(expectedSeconds, qosTimeout);\n        AssertTimeout(invoker, expectedSeconds);\n        _ocelotLogger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()),\n            Times.Exactly(loggedCount));\n        var message = fMsg?.Invoke() ?? string.Empty;\n        Assert.Equal(expectedMessage, message);\n    }\n\n    [Theory]\n    [Trait(\"PR\", \"2073\")]\n    [Trait(\"Feat\", \"1314\")]\n    [Trait(\"Feat\", \"1869\")]\n    [InlineData(1, 2, \"is shorter than\")]\n    [InlineData(2, 2, \"is equal to\")]\n    [InlineData(3, 2, \"is longer than\")]\n    public void EqualitySentence_ThreeCases(int left, int right, string expected)\n    {\n        // Arrange, Act\n        var actual = MessageInvokerPool.EqualitySentence(left, right);\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n    #endregion\n\n    [Fact]\n    public void CreateHandler_DangerousAcceptAnyServerCertificateValidatorIsTrue_InitializedRemoteCertificateValidationCallback()\n    {\n        // Arrange\n        const bool DangerousAcceptAnyServerCertificateValidator = true;\n        var route = GivenRoute(null, null, DangerousAcceptAnyServerCertificateValidator);\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        GivenARequest(route, PortFinder.GetRandomPort());\n        Func<string> fMsg = null;\n        _ocelotLogger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(f => fMsg = f);\n\n        // Act\n        using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n        // Assert\n        _ocelotLogger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var message = fMsg?.Invoke() ?? string.Empty;\n        Assert.Equal(\"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute -> /\", message);\n\n        var handler = AssertTimeoutDelegatingHandler(invoker);\n        var baseHandler = handler.InnerHandler as SocketsHttpHandler;\n        Assert.NotNull(baseHandler.CookieContainer);\n        Assert.NotNull(baseHandler?.SslOptions?.RemoteCertificateValidationCallback);\n        bool alwaysTrue = baseHandler.SslOptions.RemoteCertificateValidationCallback.Invoke(this, null, null, SslPolicyErrors.None);\n        Assert.True(alwaysTrue);\n    }\n\n    [Fact]\n    public void Clear_WithOneItem_HandlersPoolShouldBeEmpty()\n    {\n        // Arrange\n        var route = GivenRoute(null, null);\n        GivenTheFactoryReturns(new());\n        GivenAMessageInvokerPool();\n        GivenARequest(route, PortFinder.GetRandomPort());\n\n        // Act, Assert 1\n        using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n        Assert.NotNull(invoker);\n        Type me = _pool.GetType();\n        var field = me.GetField(\"_handlersPool\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        var handlersPool = field.GetValue(_pool) as ConcurrentDictionary<MessageInvokerPool.MessageInvokerCacheKey, Lazy<HttpMessageInvoker>>;\n        Assert.NotNull(handlersPool);\n        Assert.NotEmpty(handlersPool);\n        Assert.Single(handlersPool);\n\n        // Act, Assert 2\n        _pool.Clear();\n        Assert.Empty(handlersPool);\n        _ocelotLogger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Never());\n    }\n\n    private void AndAHandlerFactory() => _handlerFactory = GetHandlerFactory();\n\n    private async Task WhenICallTheClient(string url)\n    {\n        var messageInvoker = _pool.Get(_context.Items.DownstreamRoute());\n        _response = await messageInvoker\n            .SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None);\n    }\n\n    private static void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) =>\n        fakeOne.TimeCalled.ShouldBeGreaterThan(fakeTwo.TimeCalled);\n\n    private static Mock<IDelegatingHandlerFactory> GetHandlerFactory()\n    {\n        var handlerFactory = new Mock<IDelegatingHandlerFactory>();\n        handlerFactory.Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(new List<DelegatingHandler>());\n        return handlerFactory;\n    }\n\n    private static DownstreamRoute DownstreamRouteFactory(string path) => new DownstreamRouteBuilder()\n            .WithDownstreamPathTemplate(path)\n            .WithQosOptions(new QoSOptions(new FileQoSOptions()))\n            .WithLoadBalancerKey(string.Empty)\n            .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build())\n            .WithHttpHandlerOptions(new() { MaxConnectionsPerServer = 10, PooledConnectionLifeTime = TimeSpan.FromSeconds(120) })\n            .WithUpstreamHttpMethod([\"Get\"])\n            .Build();\n\n    [Collection(nameof(SequentialTests))]\n    public sealed class Sequential : MessageInvokerPoolBase\n    {\n        [Fact]\n        [Trait(\"Bug\", \"1833\")]\n        public void SendAsync_NoQosAndNoRouteTimeouts_ShouldTimeoutAfterDefaultSeconds()\n        {\n            // Arrange\n            var route = GivenRoute(null, null);\n            GivenTheFactoryReturnsNothing();\n            GivenAMessageInvokerPool();\n            GivenARequest(route, PortFinder.GetRandomPort());\n\n            // Act, Assert\n            DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // minimum possible\n            try\n            {\n                //int marginMs = 50;\n                //var expected = TimeSpan.FromSeconds(DownstreamRoute.LowTimeout);\n                //var watcher = await TestRetry.NoWaitAsync(\n                //    () => WhenICallTheClientWillThrowAfterTimeout(expected, marginMs *= 2)); // call up to 3 times with margins 100, 200, 400\n                //AssertTimeoutPrecisely(watcher, expected);\n                // Act\n                using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n                // Assert\n                AssertTimeout(invoker, DownstreamRoute.LowTimeout);\n            }\n            finally\n            {\n                DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout;\n            }\n        }\n\n        [Fact]\n        [Trait(\"PR\", \"2073\")]\n        [Trait(\"Feat\", \"1314\")]\n        [Trait(\"Feat\", \"1869\")]\n        public void EnsureRouteTimeoutIsGreaterThanQosOne_RouteQosTimeoutIsGreaterThanRouteOne_EnsuredQos()\n        {\n            // Arrange\n            var route = GivenRoute(DownstreamRoute.LowTimeout + 1, null);\n            GivenTheFactoryReturnsNothing();\n            GivenAMessageInvokerPool();\n            GivenARequest(route, PortFinder.GetRandomPort());\n            Func<string> fMsg = null;\n            _ocelotLogger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n                .Callback<Func<string>>(f => fMsg = f);\n\n            // Act, Assert\n            DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // minimum possible\n            try\n            {\n                // Act\n                using var invoker = _pool.Get(_context.Items.DownstreamRoute());\n\n                // Assert\n                AssertTimeout(invoker, 8); // should have doubled QoS timeout\n                _ocelotLogger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n                var message = fMsg?.Invoke() ?? string.Empty;\n                Assert.Equal(\"Route '/' has Quality of Service settings (QoSOptions) enabled, but either the DownstreamRoute.DefaultTimeoutSeconds or the QoS Timeout is misconfigured: specifically, the DownstreamRoute.DefaultTimeoutSeconds (3000 ms) is shorter than the QoS Timeout (4000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 8000 ms to the route Timeout instead of using DownstreamRoute.DefaultTimeoutSeconds. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!\", message);\n            }\n            finally\n            {\n                DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout;\n            }\n        }\n    }\n}\n\npublic class MessageInvokerPoolBase : UnitTest\n{\n    protected Mock<IDelegatingHandlerFactory> _handlerFactory;\n    protected HttpResponseMessage _response;\n    protected MessageInvokerPool _pool;\n\n    protected readonly DefaultHttpContext _context = new();\n    protected readonly Mock<IOcelotLogger> _ocelotLogger = new();\n    protected readonly Mock<IOcelotLoggerFactory> _ocelotLoggerFactory = new();\n\n    public MessageInvokerPoolBase()\n    {\n        _ocelotLoggerFactory.Setup(x => x.CreateLogger<MessageInvokerPool>()).Returns(_ocelotLogger.Object);\n    }\n\n    public static int Ms(int seconds) => 1000 * seconds;\n\n    protected static DownstreamRoute GivenRoute(int? qosTimeout, int? routeTimeout,\n        bool dangerousAcceptAnyServerCertificateValidator = false)\n    {\n        var qosOptions = new QoSOptions(qosTimeout.HasValue ? Ms(qosTimeout.Value) : null); // !!!\n        var handlerOptions = new HttpHandlerOptions()\n        {\n            MaxConnectionsPerServer = int.MaxValue,\n            UseCookieContainer = true,\n        };\n        var route = new DownstreamRouteBuilder()\n            .WithQosOptions(qosOptions)\n            .WithHttpHandlerOptions(handlerOptions)\n            .WithTimeout(routeTimeout) // !!!\n            .WithUpstreamPathTemplate(new(\"/\", 0, false, \"/\"))\n            .WithDangerousAcceptAnyServerCertificateValidator(dangerousAcceptAnyServerCertificateValidator)\n            .Build();\n        return route;\n    }\n\n    protected void GivenTheFactoryReturnsNothing()\n    {\n        var nothing = new List<DelegatingHandler>();\n        GivenTheFactoryReturns(nothing);\n    }\n\n    protected void GivenTheFactoryReturns(List<DelegatingHandler> handlers)\n    {\n        _handlerFactory = new Mock<IDelegatingHandlerFactory>();\n        _handlerFactory.Setup(x => x.Get(It.IsAny<DownstreamRoute>()))\n            .Returns(handlers);\n    }\n\n    protected void GivenAMessageInvokerPool() =>\n        _pool = new MessageInvokerPool(_handlerFactory.Object, _ocelotLoggerFactory.Object);\n\n    protected void GivenARequest(DownstreamRoute downstream, int port)\n        => GivenARequestWithAUrlAndMethod(downstream, Url(port), HttpMethod.Get);\n    protected void GivenARequestWithAUrlAndMethod(DownstreamRoute downstream, string url, HttpMethod method)\n    {\n        _context.Items.UpsertDownstreamRoute(downstream);\n        _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage\n        { RequestUri = new Uri(url), Method = method }));\n    }\n\n    protected async Task<Stopwatch> WhenICallTheClientWillThrowAfterTimeout(TimeSpan timeout, int marginMilliseconds)\n    {\n        var messageInvoker = _pool.Get(_context.Items.DownstreamRoute());\n        var stopwatch = new Stopwatch();\n        stopwatch.Start();\n        try\n        {\n            _response = await messageInvoker\n                .SendAsync(new HttpRequestMessage(HttpMethod.Get, \"http://savsgbfgnsgndg.com\"), CancellationToken.None);\n        }\n        catch (Exception e)\n        {\n            Assert.IsType<TimeoutException>(e);\n        }\n\n        // Compare the elapsed time with the given timeout\n        // You can use elapsed.CompareTo(timeout) or simply check if elapsed > timeout, based on your requirement\n        stopwatch.Stop();\n        var elapsed = stopwatch.Elapsed;\n        var margin = TimeSpan.FromMilliseconds(marginMilliseconds);\n        Assert.True(elapsed >= timeout.Subtract(margin), $\"Elapsed time {elapsed} is smaller than expected timeout {timeout} - {marginMilliseconds}ms\");\n        Assert.True(elapsed < timeout.Add(margin), $\"Elapsed time {elapsed} is bigger than expected timeout {timeout} + {marginMilliseconds}ms\");\n        return stopwatch;\n    }\n\n    protected static void AssertTimeoutPrecisely(Stopwatch watcher, TimeSpan expected, TimeSpan? precision = null)\n    {\n        precision ??= TimeSpan.FromMilliseconds(10);\n        TimeSpan elapsed = watcher.Elapsed, margin = elapsed - expected;\n        try\n        {\n            Assert.True(elapsed >= expected, $\"Elapsed time {elapsed} is less than expected timeout {expected} with margin {margin}.\");\n        }\n        catch (TrueException)\n        {\n            // The elapsed time is approximately 0.998xxx or 2.99xxx, with a 10ms margin of precision accepted.\n            Assert.True(elapsed.Add(precision.Value) >= expected, $\"Elapsed time {elapsed} is less than expected timeout {expected} with margin {margin} which module is >= {precision.Value.Milliseconds}ms.\");\n        }\n    }\n\n    protected static TimeoutDelegatingHandler AssertTimeoutDelegatingHandler(HttpMessageInvoker invoker)\n    {\n        Assert.NotNull(invoker);\n        Type me = invoker.GetType();\n        var field = me.GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        var handler = field.GetValue(invoker) as HttpMessageHandler;\n        Assert.NotNull(handler);\n        Assert.IsType<TimeoutDelegatingHandler>(handler);\n        return handler as TimeoutDelegatingHandler;\n    }\n\n    protected static TimeSpan AssertTimeout(HttpMessageInvoker invoker, int expectedSeconds)\n    {\n        var handler = AssertTimeoutDelegatingHandler(invoker);\n        var me = handler.GetType();\n        var field = me.GetField(\"_timeout\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        var timeout = (TimeSpan)field.GetValue(handler);\n        Assert.Equal(expectedSeconds, (int)timeout.TotalSeconds);\n        return timeout;\n    }\n\n    protected static string Url(int port) => $\"http://localhost:{port}\";\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Requester/TimeoutDelegatingHandlerTests.cs",
    "content": "﻿using Ocelot.Requester;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.Requester;\n\npublic sealed class TimeoutDelegatingHandlerTests : UnitTest\n{\n    [Fact]\n    public async Task SendAsync_OnTimeout_ShouldThrowTimeoutException()\n    {\n        // Arrange\n        int ms = 100;\n        using var baseHandler = new SocketsHttpHandler();\n        using var handler = new TimeoutDelegatingHandler(TimeSpan.FromMilliseconds(ms));\n        handler.InnerHandler = baseHandler;\n\n        var type = handler.GetType();\n        var method = type.GetMethod(\"SendAsync\", BindingFlags.Instance | BindingFlags.NonPublic);\n\n        var request = new HttpRequestMessage(HttpMethod.Get, \"https://www.nuget.org/\");\n        using var cts = new CancellationTokenSource();\n        CancellationToken token = cts.Token;\n\n        // Act\n        var args = new object[] { request, cts.Token };\n        Task<HttpResponseMessage> sendAsync() => (Task<HttpResponseMessage>)method.Invoke(handler, args);\n        async Task sendAsyncAndWaitForTimeout(int delay)\n        {\n            await sendAsync();\n            await Task.Delay(delay); // wait for Timeout event\n        }\n\n        // Assert\n        ms += IsCiCd() ? 50 : 0;\n        var ex = await Assert.ThrowsAsync<TimeoutException>(() => sendAsyncAndWaitForTimeout(ms));\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Responder/AnyError.cs",
    "content": "﻿using Ocelot.Errors;\n\nnamespace Ocelot.UnitTests.Responder;\n\ninternal class AnyError : Error\n{\n    public AnyError() : base(\"blahh\", OcelotErrorCode.UnknownError, 404)\n    { }\n\n    public AnyError(OcelotErrorCode errorCode) : base(\"blah\", errorCode, 404)\n    { }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs",
    "content": "﻿using Ocelot.Errors;\r\nusing Ocelot.Responder;\r\nnamespace Ocelot.UnitTests.Responder;\r\n\r\npublic class ErrorsToHttpStatusCodeMapperTests : UnitTest\r\n{\r\n    private readonly ErrorsToHttpStatusCodeMapper _codeMapper = new();\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.UnauthenticatedError)]\r\n    public void Should_return_unauthorized(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Unauthorized);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.CannotFindClaimError)]\r\n    [InlineData(OcelotErrorCode.ClaimValueNotAuthorizedError)]\r\n    [InlineData(OcelotErrorCode.ScopeNotAuthorizedError)]\r\n    [InlineData(OcelotErrorCode.UnauthorizedError)]\r\n    [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)]\r\n    public void Should_return_forbidden(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Forbidden);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.RequestTimedOutError)]\r\n    public void Should_return_service_unavailable(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.ServiceUnavailable);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.UnableToCompleteRequestError)]\r\n    [InlineData(OcelotErrorCode.CouldNotFindLoadBalancerCreator)]\r\n    [InlineData(OcelotErrorCode.ErrorInvokingLoadBalancerCreator)]\r\n    public void Should_return_internal_server_error(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.InternalServerError);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.ConnectionToDownstreamServiceError)]\r\n    public void Should_return_bad_gateway_error(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.BadGateway);\r\n    }\r\n\r\n    [Theory]\r\n    [InlineData(OcelotErrorCode.CannotAddDataError)]\r\n    [InlineData(OcelotErrorCode.CannotFindDataError)]\r\n    [InlineData(OcelotErrorCode.DownstreamHostNullOrEmptyError)]\r\n    [InlineData(OcelotErrorCode.DownstreamPathNullOrEmptyError)]\r\n    [InlineData(OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)]\r\n    [InlineData(OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)]\r\n    [InlineData(OcelotErrorCode.DownstreamSchemeNullOrEmptyError)]\r\n    [InlineData(OcelotErrorCode.FileValidationFailedError)]\r\n    [InlineData(OcelotErrorCode.InstructionNotForClaimsError)]\r\n    [InlineData(OcelotErrorCode.NoInstructionsError)]\r\n    [InlineData(OcelotErrorCode.ParsingConfigurationHeaderError)]\r\n    [InlineData(OcelotErrorCode.RateLimitOptionsError)]\r\n    [InlineData(OcelotErrorCode.ServicesAreEmptyError)]\r\n    [InlineData(OcelotErrorCode.ServicesAreNullError)]\r\n    [InlineData(OcelotErrorCode.UnableToCreateAuthenticationHandlerError)]\r\n    [InlineData(OcelotErrorCode.UnableToFindDownstreamRouteError)]\r\n    [InlineData(OcelotErrorCode.UnableToFindLoadBalancerError)]\r\n    [InlineData(OcelotErrorCode.UnableToFindServiceDiscoveryProviderError)]\r\n    [InlineData(OcelotErrorCode.UnableToFindQoSProviderError)]\r\n    [InlineData(OcelotErrorCode.UnknownError)]\r\n    [InlineData(OcelotErrorCode.UnmappableRequestError)]\r\n    [InlineData(OcelotErrorCode.UnsupportedAuthenticationProviderError)]\r\n    public void Should_return_not_found(OcelotErrorCode errorCode)\r\n    {\r\n        ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.NotFound);\r\n    }\n\r\n    [Fact]\r\n    [Trait(\"Bug\", \"749\")] // https://github.com/ThreeMammals/Ocelot/issues/749\r\n    [Trait(\"PR\", \"1769\")] // https://github.com/ThreeMammals/Ocelot/pull/1769\n    public void Should_return_request_entity_too_large()\n    {\n        ShouldMapErrorsToStatusCode(new() { OcelotErrorCode.PayloadTooLargeError }, HttpStatusCode.RequestEntityTooLarge);\n    }\n\r\n    [Fact]\r\n    public void AuthenticationErrorsHaveHighestPriority()\r\n    {\r\n        var errors = new List<OcelotErrorCode>\r\n        {\r\n            OcelotErrorCode.CannotAddDataError,\r\n            OcelotErrorCode.CannotFindClaimError,\r\n            OcelotErrorCode.UnauthenticatedError,\r\n            OcelotErrorCode.RequestTimedOutError,\r\n        };\r\n\r\n        ShouldMapErrorsToStatusCode(errors, HttpStatusCode.Unauthorized);\r\n    }\r\n\r\n    [Fact]\r\n    public void AuthorizationErrorsHaveSecondHighestPriority()\r\n    {\r\n        var errors = new List<OcelotErrorCode>\r\n        {\r\n            OcelotErrorCode.CannotAddDataError,\r\n            OcelotErrorCode.CannotFindClaimError,\r\n            OcelotErrorCode.RequestTimedOutError,\r\n        };\r\n\r\n        ShouldMapErrorsToStatusCode(errors, HttpStatusCode.Forbidden);\r\n    }\r\n\r\n    [Fact]\r\n    public void ServiceUnavailableErrorsHaveThirdHighestPriority()\r\n    {\r\n        var errors = new List<OcelotErrorCode>\r\n        {\r\n            OcelotErrorCode.CannotAddDataError,\r\n            OcelotErrorCode.RequestTimedOutError,\r\n        };\r\n\r\n        ShouldMapErrorsToStatusCode(errors, HttpStatusCode.ServiceUnavailable);\r\n    }\r\n\r\n    [Fact]\r\n    public void Check_we_have_considered_all_errors_in_these_tests()\r\n    {\r\n        // If this test fails then it's because the number of error codes has changed.\r\n        // You should make the appropriate changes to the test cases here to ensure\r\n        // they cover all the error codes, and then modify this assertion.\r\n        Enum.GetNames<OcelotErrorCode>().Length.ShouldBe(42, \"Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?\");\r\n    }\r\n\r\n    private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)\r\n    {\r\n        ShouldMapErrorsToStatusCode(new List<OcelotErrorCode> { errorCode }, expectedHttpStatusCode);\r\n    }\r\n\r\n    private void ShouldMapErrorsToStatusCode(List<OcelotErrorCode> errorCodes, HttpStatusCode expectedHttpStatusCode)\r\n    {\r\n        // Arrange\r\n        var errors = new List<Error>();\r\n        foreach (var errorCode in errorCodes)\r\n        {\r\n            errors.Add(new AnyError(errorCode));\r\n        }\r\n\r\n        // Act\r\n        var result = _codeMapper.Map(errors);\r\n\r\n        // Assert\r\n        result.ShouldBe((int)expectedHttpStatusCode);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Ocelot.Headers;\nusing Ocelot.Middleware;\nusing Ocelot.Responder;\n\nnamespace Ocelot.UnitTests.Responder;\n\npublic class HttpContextResponderTests\n{\n    private readonly HttpContextResponder _responder;\n\n    public HttpContextResponderTests()\n    {\n        var removeOutputHeaders = new RemoveOutputHeaders();\n        _responder = new HttpContextResponder(removeOutputHeaders);\n    }\n\n    [Fact]\n    public async Task Should_remove_transfer_encoding_header()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"Transfer-Encoding\", new List<string> {\"woop\"}),\n            }, \"some reason\");\n\n        // Act\n        await _responder.SetResponseOnHttpContext(httpContext, response);\n\n        // Assert\n        var header = httpContext.Response.Headers.TransferEncoding;\n        header.ShouldBeEmpty();\n    }\n\n    [Fact]\n    public async Task Should_ignore_content_if_null()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        var response = new DownstreamResponse(null, HttpStatusCode.OK,\n            new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\");\n\n        // Assert\n        await Should.NotThrowAsync(async () =>\n        {\n            // Act\n            await _responder.SetResponseOnHttpContext(httpContext, response);\n        });\n    }\n\n    [Fact]\n    public async Task Should_have_content_length()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        var response = new DownstreamResponse(new StringContent(\"test\"), HttpStatusCode.OK,\n            new List<KeyValuePair<string, IEnumerable<string>>>(), \"some reason\");\n\n        // Act\n        await _responder.SetResponseOnHttpContext(httpContext, response);\n\n        // Assert\n        var header = httpContext.Response.Headers[\"Content-Length\"];\n        header.First().ShouldBe(\"4\");\n    }\n\n    [Fact]\n    public async Task Should_add_header()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"test\", new List<string> {\"test\"}),\n            }, \"some reason\");\n\n        // Act\n        await _responder.SetResponseOnHttpContext(httpContext, response);\n\n        // Assert\n        var header = httpContext.Response.Headers[\"test\"];\n        header.First().ShouldBe(\"test\");\n    }\n\n    [Fact]\n    public async Task Should_add_reason_phrase()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n        var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK,\n            new List<KeyValuePair<string, IEnumerable<string>>>\n            {\n                new(\"test\", new List<string> {\"test\"}),\n            }, \"some reason\");\n\n        // Act\n        await _responder.SetResponseOnHttpContext(httpContext, response);\n\n        // Assert\n        httpContext.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase.ShouldBe(response.ReasonPhrase);\n    }\n\n    [Fact]\n    public void Should_call_without_exception()\n    {\n        // Arrange\n        var httpContext = new DefaultHttpContext();\n\n        // Act, Assert\n        _responder.SetErrorResponseOnContext(httpContext, 500);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.DownstreamRouteFinder.Finder;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Responder;\nusing Ocelot.Responder.Middleware;\n\nnamespace Ocelot.UnitTests.Responder;\n\npublic class ResponderMiddlewareTests : UnitTest\n{\n    private readonly Mock<IHttpResponder> _responder;\n    private readonly Mock<IErrorsToHttpStatusCodeMapper> _codeMapper;\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly ResponderMiddleware _middleware;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _httpContext;\n\n    public ResponderMiddlewareTests()\n    {\n        _httpContext = new DefaultHttpContext();\n        _responder = new Mock<IHttpResponder>();\n        _codeMapper = new Mock<IErrorsToHttpStatusCodeMapper>();\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<ResponderMiddleware>()).Returns(_logger.Object);\n        _next = context => Task.CompletedTask;\n        _middleware = new ResponderMiddleware(_next, _responder.Object, _loggerFactory.Object, _codeMapper.Object);\n    }\n\n    [Fact]\n    public async Task Should_not_return_any_errors()\n    {\n        // Arrange\n        _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage()));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.Errors().ShouldBeEmpty();\n    }\n\n    [Fact]\n    public async Task Should_return_any_errors()\n    {\n        // Arrange\n        _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage()));\n        _httpContext.Items.SetError(new UnableToFindDownstreamRouteError(\"/path\", \"GET\"));\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.Errors().Count.ShouldBe(1);\n    }\n\n    [Fact]\n    public async Task Should_not_call_responder_when_null_downstream_response()\n    {\n        // Arrange\n        this._responder.Reset();\n        _httpContext.Items.UpsertDownstreamResponse(null);\n\n        // Act\n        await _middleware.Invoke(_httpContext);\n\n        // Assert\n        _httpContext.Items.Errors().ShouldBeEmpty();\n        _responder.VerifyNoOtherCalls();\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Configuration.Creator;\nusing Ocelot.Configuration.File;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.Responses;\nusing Ocelot.Security.IPSecurity;\n\nnamespace Ocelot.UnitTests.Security;\n\npublic sealed class IPSecurityPolicyTests : UnitTest\n{\n    private readonly DownstreamRouteBuilder _downstreamRouteBuilder;\n    private readonly IPSecurityPolicy _policy;\n    private readonly DefaultHttpContext _context;\n    private readonly SecurityOptionsCreator _securityOptionsCreator;\n    private static readonly FileGlobalConfiguration Empty = new();\n\n    public IPSecurityPolicyTests()\n    {\n        _context = new DefaultHttpContext();\n        _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        _downstreamRouteBuilder = new DownstreamRouteBuilder();\n        _policy = new IPSecurityPolicy();\n        _securityOptionsCreator = new SecurityOptionsCreator();\n    }\n\n    [Fact]\n    public void Should_No_blocked_Ip_and_allowed_Ip()\n    {\n        // Arrange, Act\n        var actual = WhenTheSecurityPolicy(new());\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_blockedIp_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.1\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_blockedIp_clientIp_Not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.2\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.1\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_allowedIp_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.1\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_allowedIp_clientIp_Not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.2\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.1\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_cidrNotation_allowed24_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.10.5\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0/24\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_cidrNotation_allowed24_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.5\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0/24\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_cidrNotation_allowed29_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.10\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0/29\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_cidrNotation_blocked24_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.1\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0/24\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_cidrNotation_blocked24_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.10.1\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0/24\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_range_allowed_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0-192.168.1.10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_range_allowed_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.8\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0-192.168.1.10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_range_blocked_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.5\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0-192.168.1.10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_range_blocked_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0-192.168.1.10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_shortRange_allowed_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0-10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_shortRange_allowed_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.8\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0-10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_shortRange_blocked_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.5\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0-10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_shortRange_blocked_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0-10\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_ipSubnet_allowed_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.10.15\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0/255.255.255.0\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_ipSubnet_allowed_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.0/255.255.255.0\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_ipSubnet_blocked_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.15\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0/255.255.255.0\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_ipSubnet_blocked_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.10.1\")[0];\n        var options = new FileSecurityOptions(blockedIPs: \"192.168.1.0/255.255.255.0\");\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_exludeAllowedFromBlocked_moreAllowed_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.150\")[0];\n        var options = new FileSecurityOptions(\"192.168.0.0/255.255.0.0\", \"192.168.1.100-200\", false);\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_exludeAllowedFromBlocked_moreAllowed_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.150\")[0];\n        var options = new FileSecurityOptions(\"192.168.0.0/255.255.0.0\", \"192.168.1.100-200\", true);\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_exludeAllowedFromBlocked_moreBlocked_clientIp_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.10\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.10-20\", \"192.168.1.0/23\", false);\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.True(actual.IsError);\n    }\n\n    [Fact]\n    public void Should_exludeAllowedFromBlocked_moreBlocked_clientIp_not_block()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.10\")[0];\n        var options = new FileSecurityOptions(\"192.168.1.10-20\", \"192.168.1.0/23\", true);\n\n        // Act\n        var actual = WhenTheSecurityPolicy(options);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    [Fact]\n    [Trait(\"Feat\", \"2170\")]\n    public void Should_route_config_overrides_global_config()\n    {\n        // Arrange\n        _context.Connection.RemoteIpAddress = Dns.GetHostAddresses(\"192.168.1.10\")[0];\n        var globalConfig = new FileGlobalConfiguration\n        {\n            SecurityOptions = new FileSecurityOptions(\"192.168.1.30-50\", \"192.168.1.1-100\", true),\n        };\n        var localConfig = new FileSecurityOptions(\"192.168.1.10\", \"\", false);\n\n        // Act\n        var actual = WhenTheSecurityPolicy(localConfig, globalConfig);\n\n        // Assert\n        Assert.False(actual.IsError);\n    }\n\n    private Response WhenTheSecurityPolicy(FileSecurityOptions options, FileGlobalConfiguration global = null)\n    {\n        // Arrange\n        var securityOptions = _securityOptionsCreator.Create(options, global ?? Empty);\n        _downstreamRouteBuilder.WithSecurityOptions(securityOptions);\n        _context.Items.UpsertDownstreamRoute(_downstreamRouteBuilder.Build());\n\n        // Act\n        return _policy.Security(_context.Items.DownstreamRoute(), _context);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Ocelot.Errors;\r\nusing Ocelot.Logging;\r\nusing Ocelot.Middleware;\r\nusing Ocelot.Request.Middleware;\r\nusing Ocelot.Responses;\r\nusing Ocelot.Security;\r\nusing Ocelot.Security.Middleware;\n\r\nnamespace Ocelot.UnitTests.Security;\r\n\r\npublic sealed class SecurityMiddlewareTests : UnitTest\r\n{\r\n    private readonly List<Mock<ISecurityPolicy>> _securityPolicyList;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n    private readonly SecurityMiddleware _middleware;\r\n    private readonly RequestDelegate _next;\r\n    private readonly HttpContext _httpContext;\r\n\r\n    public SecurityMiddlewareTests()\r\n    {\r\n        _httpContext = new DefaultHttpContext();\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _loggerFactory.Setup(x => x.CreateLogger<SecurityMiddleware>()).Returns(_logger.Object);\r\n        _securityPolicyList = new List<Mock<ISecurityPolicy>>\r\n        {\r\n            new(),\r\n            new(),\r\n        };\r\n        _next = context => Task.CompletedTask;\r\n        _middleware = new SecurityMiddleware(_next, _loggerFactory.Object, _securityPolicyList.Select(f => f.Object).ToList());\r\n        _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, \"http://test.com\")));\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_legal_request()\r\n    {\r\n        // Arrange\r\n        GivenPassingSecurityVerification();\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert: security passed\r\n        _httpContext.Items.Errors().Count.ShouldBe(0);\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_verification_failed_request()\r\n    {\r\n        // Arrange\r\n        GivenNotPassingSecurityVerification();\r\n\r\n        // Act\r\n        await _middleware.Invoke(_httpContext);\r\n\r\n        // Assert: security not passed\r\n        _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0);\r\n    }\r\n\r\n    private void GivenPassingSecurityVerification()\r\n    {\r\n        foreach (var item in _securityPolicyList)\r\n        {\r\n            Response response = new OkResponse();\r\n            item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response);\r\n        }\r\n    }\r\n\r\n    private void GivenNotPassingSecurityVerification()\r\n    {\r\n        for (var i = 0; i < _securityPolicyList.Count; i++)\r\n        {\r\n            var item = _securityPolicyList[i];\r\n            if (i == 0)\r\n            {\r\n                Error error = new UnauthenticatedError(\"Not passing security verification\");\r\n                Response response = new ErrorResponse(error);\r\n                item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response);\r\n            }\r\n            else\r\n            {\r\n                Response response = new OkResponse();\r\n                item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/SequentialTests.cs",
    "content": "﻿namespace Ocelot.UnitTests;\n\n/// <summary>\n/// Apply <see cref=\"CollectionAttribute\"/> to classes to disable parallelization.\n/// </summary>\n[CollectionDefinition(nameof(SequentialTests), DisableParallelization = true)]\npublic class SequentialTests\n{\n    ///// <summary>\n    ///// Unstable <see cref=\"Kubernetes.KubeTests\"/>.\n    ///// </summary>\n    //[Collection(nameof(SequentialTests))]\n    //public class KubeTests : Kubernetes.KubeTests\n    //{ } // all tests\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs",
    "content": "using Ocelot.ServiceDiscovery.Providers;\r\nusing Ocelot.Values;\n\r\nnamespace Ocelot.UnitTests.ServiceDiscovery;\r\n\r\npublic class ConfigurationServiceProviderTests : UnitTest\r\n{\r\n    private ConfigurationServiceProvider _serviceProvider;\r\n\r\n    [Fact]\r\n    public async Task Should_return_services()\r\n    {\r\n        // Arrange\r\n        var hostAndPort = new ServiceHostAndPort(\"127.0.0.1\", 80);\r\n        var services = new List<Service>\r\n        {\r\n            new(\"product\", hostAndPort, string.Empty, string.Empty, Array.Empty<string>()),\r\n        };\r\n        _serviceProvider = new ConfigurationServiceProvider(services);\r\n\r\n        // Act\r\n        var result = await _serviceProvider.GetAsync();\n\r\n        // Assert\r\n        result[0].HostAndPort.DownstreamHost.ShouldBe(services[0].HostAndPort.DownstreamHost);\r\n        result[0].HostAndPort.DownstreamPort.ShouldBe(services[0].HostAndPort.DownstreamPort);\r\n        result[0].Name.ShouldBe(services[0].Name);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs",
    "content": "using KubeClient;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Provider.Kubernetes;\nusing Ocelot.Responses;\nusing Ocelot.ServiceDiscovery;\nusing Ocelot.ServiceDiscovery.Providers;\nusing Ocelot.Values;\n\r\nnamespace Ocelot.UnitTests.ServiceDiscovery;\r\n\r\npublic class ServiceDiscoveryProviderFactoryTests : UnitTest\r\n{\r\n    private Response<IServiceDiscoveryProvider> _result;\r\n    private ServiceDiscoveryProviderFactory _factory;\r\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\r\n    private readonly Mock<IOcelotLogger> _logger;\r\n    private IServiceProvider _provider;\r\n    private readonly IServiceCollection _collection;\r\n\r\n    public ServiceDiscoveryProviderFactoryTests()\r\n    {\r\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\r\n        _logger = new Mock<IOcelotLogger>();\r\n        _collection = new ServiceCollection();\r\n        _provider = _collection.BuildServiceProvider(true);\r\n        _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider);\n\n        _loggerFactory.Setup(x => x.CreateLogger<ServiceDiscoveryProviderFactory>())\n            .Returns(_logger.Object);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_no_service_provider()\r\n    {\r\n        // Arrange\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .Build();\r\n        var route = new DownstreamRouteBuilder().Build();\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        _result.Data.ShouldBeOfType<ConfigurationServiceProvider>();\r\n    }\r\n\r\n    [Fact]\r\n    public async Task Should_return_list_of_configuration_services()\r\n    {\r\n        // Arrange\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .Build();\r\n        var downstreamAddresses = new List<DownstreamHostAndPort>\r\n        {\r\n            new(\"asdf.com\", 80),\r\n            new(\"abc.com\", 80),\r\n        };\r\n        var route = new DownstreamRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build();\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        _result.Data.ShouldBeOfType<ConfigurationServiceProvider>();\r\n\r\n        // Assert: Then The Following Services Are Returned\r\n        var result = (ConfigurationServiceProvider)_result.Data;\r\n        var services = await result.GetAsync();\r\n        for (var i = 0; i < services.Count; i++)\r\n        {\r\n            var service = services[i];\r\n            var downstreamAddress = downstreamAddresses[i];\r\n\r\n            service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host);\r\n            service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port);\r\n        }\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_provider_because_type_matches_reflected_type_from_delegate()\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(\"product\")\r\n            .Build();\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithType(nameof(Fake))\r\n            .Build();\r\n        GivenAFakeDelegate();\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        _result.Data.GetType().Name.ShouldBe(\"Fake\");\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_not_return_provider_because_type_doesnt_match_reflected_type_from_delegate()\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(\"product\")\r\n            .Build();\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithType(\"Wookie\")\r\n            .Build();\r\n        GivenAFakeDelegate();\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        _result.IsError.ShouldBeTrue();\n        _result.Errors.Count.ShouldBe(1);\n\n        _logInformationMessages.ShouldNotBeNull()\n            .Count.ShouldBe(2);\n        _logger.Verify(x => x.LogInformation(It.IsAny<Func<string>>()),\n            Times.Exactly(2));\r\n\r\n        _logWarningMessages.ShouldNotBeNull()\n            .Count.ShouldBe(1);\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()),\n            Times.Once());\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_return_service_fabric_provider()\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(\"product\")\r\n            .Build();\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithType(\"ServiceFabric\")\r\n            .Build();\r\n        GivenAFakeDelegate();\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        _result.Data.ShouldBeOfType<ServiceFabricServiceDiscoveryProvider>();\r\n    }\n\r\n    [Theory]\n    [Trait(\"Bug\", \"1954\")]\n    [InlineData(\"Kube\", true)]\r\n    [InlineData(\"kube\", true)]\r\n    [InlineData(\"PollKube\", true)]\r\n    [InlineData(\"pollkube\", true)]\r\n    [InlineData(\"unknown\", false)]\r\n    public void Should_return_Kubernetes_provider_with_type_names_from_docs(string typeName, bool success)\r\n    {\r\n        // Arrange\r\n        var route = new DownstreamRouteBuilder()\r\n            .WithServiceName(TestName())\r\n            .Build();\r\n        var serviceConfig = new ServiceProviderConfigurationBuilder()\r\n            .WithType(typeName)\n            .WithPollingInterval(Timeout.Infinite)\r\n            .Build();\r\n\r\n        // Arrange: Given Kubernetes Provider\r\n        var k8sClient = new Mock<IKubeApiClient>();\n        _collection\n            .AddSingleton(KubernetesProviderFactory.Get)\n            .AddSingleton(k8sClient.Object)\n            .AddSingleton(_loggerFactory.Object);\r\n        _provider = _collection.BuildServiceProvider(true);\r\n        _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider);\r\n\r\n        // Act\r\n        WhenIGetTheServiceProvider(serviceConfig, route);\r\n\r\n        // Assert\r\n        if (success)\n        {\n            _result.ShouldBeOfType<OkResponse<IServiceDiscoveryProvider>>();\n        }\n        else\n        {\n            _result.ShouldBeOfType<ErrorResponse<IServiceDiscoveryProvider>>();\n        }\n    }\n\n    private void GivenAFakeDelegate()\r\n    {\r\n        static IServiceDiscoveryProvider fake(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute name) => new Fake();\r\n        _collection.AddSingleton((ServiceDiscoveryFinderDelegate)fake);\r\n        _provider = _collection.BuildServiceProvider(true);\r\n        _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider);\r\n    }\r\n\r\n    private class Fake : IServiceDiscoveryProvider\r\n    {\r\n        public Task<List<Service>> GetAsync() => null;\r\n    }\r\n\r\n    private readonly List<string> _logInformationMessages = new();\n    private readonly List<string> _logWarningMessages = new();\n\r\n    private void WhenIGetTheServiceProvider(ServiceProviderConfiguration serviceConfig, DownstreamRoute route)\r\n    {\n        _logger.Setup(x => x.LogInformation(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(myFunc => _logInformationMessages.Add(myFunc.Invoke()));\r\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(myFunc => _logWarningMessages.Add(myFunc.Invoke()));\r\n\n        _result = _factory.Get(serviceConfig, route);\r\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs",
    "content": "﻿using Ocelot.ServiceDiscovery.Configuration;\nusing Ocelot.ServiceDiscovery.Providers;\n\nnamespace Ocelot.UnitTests.ServiceDiscovery;\n\npublic class ServiceFabricServiceDiscoveryProviderTests : UnitTest\n{\n    [Fact]\n    public async Task Should_return_service_fabric_naming_service()\n    {\n        // Arrange\n        const string host = \"localhost\";\n        const int port = 19081;\n        const string serviceName = \"OcelotServiceApplication/OcelotApplicationService\";\n\n        // Act\n        var config = new ServiceFabricConfiguration(host, port, serviceName);\n        var provider = new ServiceFabricServiceDiscoveryProvider(config);\n        var services = await provider.GetAsync();\n\n        // Assert: Then The ServiceFabric Naming Service Is Retured\n        services.Count.ShouldBe(1);\n        services[0].HostAndPort.DownstreamHost.ShouldBe(host);\n        services[0].HostAndPort.DownstreamPort.ShouldBe(port);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs",
    "content": "using Ocelot.Values;\n\r\nnamespace Ocelot.UnitTests.ServiceDiscovery;\r\n\r\npublic class ServiceRegistryTests : UnitTest\r\n{\r\n    private Service _service;\r\n    private List<Service> _services;\r\n    private readonly ServiceRegistry _serviceRegistry;\r\n    private readonly ServiceRepository _serviceRepository;\r\n\r\n    public ServiceRegistryTests()\r\n    {\r\n        _serviceRepository = new ServiceRepository();\r\n        _serviceRegistry = new ServiceRegistry(_serviceRepository);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_register_service()\r\n    {\r\n        // Arrange\r\n        _service = new Service(\"product\", new ServiceHostAndPort(\"localhost:5000\", 80), string.Empty, string.Empty, Array.Empty<string>());\r\n\r\n        // Act\r\n        _serviceRegistry.Register(_service);\r\n\r\n        // Assert: Then The Service Is Registered\r\n        var serviceNameAndAddress = _serviceRepository.Get(_service.Name);\r\n        serviceNameAndAddress[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost);\r\n        serviceNameAndAddress[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort);\r\n        serviceNameAndAddress[0].Name.ShouldBe(_service.Name);\r\n    }\r\n\r\n    [Fact]\r\n    public void Should_lookup_service()\r\n    {\r\n        // Arrange\r\n        _service = new Service(\"product\", new ServiceHostAndPort(\"localhost:600\", 80), string.Empty, string.Empty, Array.Empty<string>());\r\n        _serviceRepository.Set(_service);\r\n\r\n        // Act\r\n        _services = _serviceRegistry.Lookup(\"product\");\r\n\r\n        // Assert\r\n        _services[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost);\r\n        _services[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort);\r\n        _services[0].Name.ShouldBe(_service.Name);\r\n    }\r\n}\r\n\r\npublic interface IServiceRegistry\r\n{\r\n    void Register(Service serviceNameAndAddress);\r\n    List<Service> Lookup(string name);\r\n}\r\n\r\npublic class ServiceRegistry : IServiceRegistry\r\n{\r\n    private readonly IServiceRepository _repository;\r\n    public ServiceRegistry(IServiceRepository repository) => _repository = repository;\n    public void Register(Service serviceNameAndAddress) => _repository.Set(serviceNameAndAddress);\r\n    public List<Service> Lookup(string name) => _repository.Get(name);\r\n}\r\n\r\npublic interface IServiceRepository\r\n{\r\n    List<Service> Get(string serviceName);\r\n    void Set(Service serviceNameAndAddress);\r\n}\r\n\r\npublic class ServiceRepository : IServiceRepository\r\n{\r\n    private readonly Dictionary<string, List<Service>> _registeredServices;\r\n    public ServiceRepository() => _registeredServices = new Dictionary<string, List<Service>>();\r\n    public List<Service> Get(string serviceName) => _registeredServices[serviceName];\r\n    public void Set(Service serviceNameAndAddress)\r\n    {\r\n        if (_registeredServices.TryGetValue(serviceNameAndAddress.Name, out var services))\r\n        {\r\n            services.Add(serviceNameAndAddress);\r\n            _registeredServices[serviceNameAndAddress.Name] = services;\r\n        }\r\n        else\r\n        {\n            _registeredServices[serviceNameAndAddress.Name] = new List<Service> { serviceNameAndAddress };\r\n        }\n    }\r\n}\r\n"
  },
  {
    "path": "test/Ocelot.UnitTests/TestRetry.cs",
    "content": "﻿using Ocelot.Infrastructure.DesignPatterns;\nusing Ocelot.Logging;\n\nnamespace Ocelot.UnitTests;\n\npublic static class TestRetry\n{\n    public static TResult NoWait<TResult>(\n        Func<TResult> operation,\n        Predicate<TResult> predicate = null,\n        int retryTimes = Retry.DefaultRetryTimes,\n        IOcelotLogger logger = null)\n        => Retry.Operation(operation, predicate, retryTimes, 0, logger);\n\n    public static Task<TResult> NoWaitAsync<TResult>(\n        Func<Task<TResult>> operation,\n        Predicate<TResult> predicate = null,\n        int retryTimes = Retry.DefaultRetryTimes,\n        IOcelotLogger logger = null)\n        => Retry.OperationAsync(operation, predicate, retryTimes, 0, logger);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/UnitTest.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing Ocelot.Infrastructure.Extensions;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.UnitTests;\n\npublic class UnitTest : Unit\n{\n    //protected static FileRouteBox<FileRoute> Box(FileRoute route) => new(route);\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/UnitTests.runsettings",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<RunSettings>\r\n  <DataCollectionRunSettings>\r\n    <DataCollectors>\r\n      <DataCollector friendlyName=\"XPlat code coverage\">\r\n        <Configuration>\r\n          <Format>opencover</Format>\r\n          <SingleHit>false</SingleHit>\r\n          <UseSourceLink>true</UseSourceLink>\r\n        </Configuration>\r\n      </DataCollector>\r\n    </DataCollectors>\r\n  </DataCollectionRunSettings>\r\n  <!-- This is a workaround for an issue with Coverlet.Collector > 1.0.0 (see https://github.com/microsoft/vstest/issues/2205)-->\r\n  <InProcDataCollectionRunSettings>\r\n    <InProcDataCollectors>\r\n      <InProcDataCollector assemblyQualifiedName=\"Coverlet.Collector.DataCollection.CoverletInProcDataCollector, coverlet.collector, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null\"\r\n                     friendlyName=\"XPlat Code Coverage\"\r\n                     enabled=\"True\"\r\n                     codebase=\"coverlet.collector.dll\" />\r\n    </InProcDataCollectors>\r\n  </InProcDataCollectionRunSettings>\r\n</RunSettings>\n"
  },
  {
    "path": "test/Ocelot.UnitTests/Usings.cs",
    "content": "﻿// Default Microsoft.NET.Sdk namespaces\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\n// Project extra global namespaces\nglobal using Moq;\nglobal using Ocelot;\nglobal using Ocelot.Testing;\nglobal using Ocelot.Testing.Boxing;\nglobal using Shouldly;\nglobal using System.Net;\nglobal using Xunit;\n\nusing System.Diagnostics.CodeAnalysis;\n\n[assembly: SuppressMessage(\"Usage\", \"xUnit1004:Test methods should not be skipped\", Justification = \"Reviewed.\")]\n\ninternal class Usings { }\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/ClientWebSocketConnectorTests.cs",
    "content": "﻿using Ocelot.WebSockets;\nusing System.Net.Security;\nusing System.Net.WebSockets;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\npublic sealed class ClientWebSocketConnectorTests : UnitTest, IDisposable\n{\n    private readonly ClientWebSocket _injectee; // no mocking\n    private readonly ClientWebSocketConnector _connector;\n    public ClientWebSocketConnectorTests()\n    {\n        _injectee = new(); // no mocking\n        _connector = new(_injectee);\n    }\n\n    public void Dispose() => _injectee.Dispose();\n\n    [Fact]\n    public void ToWebSocket_ReturnedConcrete()\n    {\n        // Arrange, Act\n        var actual = _connector.ToWebSocket();\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<ClientWebSocket>(actual);\n        Assert.Equal(_injectee, actual);\n    }\n\n    [Fact]\n    public void Options_ReturnedProxy()\n    {\n        // Arrange\n        static bool RemoteCertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)\n            => true;\n        _injectee.Options.RemoteCertificateValidationCallback = RemoteCertificateValidation;\n\n        // Act\n        var actual = _connector.Options;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<ClientWebSocketOptionsProxy>(actual);\n        Assert.Equal(RemoteCertificateValidation, actual.RemoteCertificateValidationCallback);\n    }\n\n    [Fact]\n    public async Task ConnectAsync_Proxied()\n    {\n        // Arrange, Act\n        var url = new UriBuilder(Uri.UriSchemeWss, \"echo.websocket.org\");\n        await _connector.ConnectAsync(url.Uri, CancellationToken.None);\n\n        // Assert\n        Assert.Equal(WebSocketState.Open, _injectee.State);\n        await _injectee.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, nameof(ConnectAsync_Proxied), CancellationToken.None);\n        Assert.Equal(WebSocketState.CloseSent, _injectee.State);\n        await _injectee.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(ConnectAsync_Proxied), CancellationToken.None);\n        Assert.Equal(WebSocketState.Closed, _injectee.State);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/ClientWebSocketOptionsProxyTests.cs",
    "content": "﻿using Ocelot.WebSockets;\nusing System.Net.Security;\nusing System.Net.WebSockets;\nusing System.Reflection;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\npublic sealed class ClientWebSocketOptionsProxyTests : UnitTest, IDisposable\n{\n    private readonly ClientWebSocket _socket;\n    private readonly ClientWebSocketOptionsProxy _proxy;\n\n    public ClientWebSocketOptionsProxyTests()\n    {\n        _socket = new ClientWebSocket();\n        _proxy = new ClientWebSocketOptionsProxy(_socket.Options);\n    }\n\n    public void Dispose()\n    {\n        _socket.Dispose();\n    }\n\n    [Fact]\n    public void HttpVersion_Proxied()\n    {\n        // Arrange\n        var expected = new Version(1, 22, 333, 4444);\n        _socket.Options.HttpVersion = expected;\n\n        // Act\n        var actual = _proxy.HttpVersion;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void HttpVersionPolicy_Proxied()\n    {\n        // Arrange\n        var expected = HttpVersionPolicy.RequestVersionOrHigher;\n        _socket.Options.HttpVersionPolicy = expected;\n\n        // Act\n        var actual = _proxy.HttpVersionPolicy;\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void UseDefaultCredentials_Proxied()\n    {\n        // Arrange\n        var expected = true;\n        _socket.Options.UseDefaultCredentials = expected;\n\n        // Act\n        var actual = _proxy.UseDefaultCredentials;\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void Credentials_Proxied()\n    {\n        // Arrange\n        var expected = new NetworkCredential(\"test\", nameof(Credentials_Proxied));\n        var cr = new Mock<ICredentials>();\n        cr.Setup(x => x.GetCredential(It.IsAny<Uri>(), It.IsAny<string>()))\n            .Returns(expected);\n        _socket.Options.Credentials = cr.Object;\n\n        // Act\n        var actual = _proxy.Credentials;\n        var actualCredential = actual.GetCredential(new(\"https://ocelot.net\"), string.Empty);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.NotNull(actualCredential);\n        Assert.Equal(expected, actualCredential);\n        Assert.Equal(\"test\", actualCredential.UserName);\n        Assert.Equal(nameof(Credentials_Proxied), actualCredential.Password);\n    }\n\n    [Fact]\n    public void Proxy_Proxied()\n    {\n        // Arrange\n        var expected = new Uri(\"https://ocelot.net\");\n        var pr = new Mock<IWebProxy>();\n        pr.Setup(x => x.GetProxy(It.IsAny<Uri>()))\n            .Returns(new Uri(\"https://ocelot.net\"));\n        _socket.Options.Proxy = pr.Object;\n\n        // Act\n        var actual = _proxy.Proxy;\n        var actualProxy = actual.GetProxy(new Uri(\"https://ocelot.blog\"));\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.NotNull(actualProxy);\n        Assert.Equal(expected, actualProxy);\n        Assert.Equal(expected.Host, actualProxy.Host);\n    }\n\n    [Fact]\n    public void ClientCertificates_Proxied()\n    {\n        // Arrange\n#pragma warning disable SYSLIB0026 // Type or member is obsolete\n        var expected = new X509CertificateCollection { new() };\n        _socket.Options.ClientCertificates = expected;\n\n        // Act\n        var actual = _proxy.ClientCertificates;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual);\n        Assert.Single(actual);\n    }\n\n    [Fact]\n    public void RemoteCertificateValidationCallback_Proxied()\n    {\n        static bool FakeCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)\n            => true;\n\n        // Arrange\n        RemoteCertificateValidationCallback expected = FakeCallback;\n        _socket.Options.RemoteCertificateValidationCallback = expected;\n\n        // Act\n        var actual = _proxy.RemoteCertificateValidationCallback;\n        var actualValue = actual?.Invoke(new(), new(), new(), new());\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual);\n        Assert.True(actualValue);\n    }\n\n    [Fact]\n    public void Cookies_Proxied()\n    {\n        // Arrange\n        var expected = new CookieContainer();\n        var host = new Uri(\"https://ocelot.net\");\n        var cookie = new Cookie(\"test\", nameof(Cookies_Proxied));\n        expected.Add(host, cookie);\n        _socket.Options.Cookies = expected;\n\n        // Act\n        var actual = _proxy.Cookies;\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(expected, actual);\n        Assert.Equal(1, actual.Count);\n    }\n\n    [Fact]\n    public void KeepAliveInterval_Proxied()\n    {\n        // Arrange\n        var expected = TimeSpan.FromMilliseconds(1234);\n        _socket.Options.KeepAliveInterval = expected;\n\n        // Act\n        var actual = _proxy.KeepAliveInterval;\n\n        // Assert\n        Assert.Equal(expected, actual);\n        Assert.Equal(1234, (int)actual.TotalMilliseconds);\n    }\n\n    [Fact]\n    public void DangerousDeflateOptions_Proxied()\n    {\n        // Arrange\n        var expected = new WebSocketDeflateOptions { ClientMaxWindowBits = 12 };\n        _socket.Options.DangerousDeflateOptions = expected;\n\n        // Act\n        var actual = _proxy.DangerousDeflateOptions;\n\n        // Assert\n        Assert.Equal(expected, actual);\n        Assert.Equal(12, actual.ClientMaxWindowBits);\n    }\n\n    [Fact]\n    public void CollectHttpResponseDetails_Proxied()\n    {\n        // Arrange\n        var expected = true;\n        _socket.Options.CollectHttpResponseDetails = expected;\n\n        // Act\n        var actual = _proxy.CollectHttpResponseDetails;\n\n        // Assert\n        Assert.Equal(expected, actual);\n    }\n\n    private static readonly Type Me = typeof(ClientWebSocketOptions);\n\n    [Fact]\n    public void AddSubProtocol_Proxied()\n    {\n        // Arrange\n        var expected = nameof(AddSubProtocol_Proxied);\n\n        // Act\n        _proxy.AddSubProtocol(expected);\n\n        // Assert\n        var prop = Me.GetProperty(\"RequestedSubProtocols\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(prop);\n        var actual = prop.GetValue(_socket.Options) as List<string>;\n        Assert.NotNull(actual);\n        Assert.Contains(expected, actual);\n    }\n\n    [Fact]\n    public void SetBuffer_Proxied()\n    {\n        // Arrange\n        int expected = 1234;\n\n        // Act\n        _proxy.SetBuffer(expected, 1);\n\n        // Assert\n        var field = Me.GetField(\"_receiveBufferSize\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        int actual = (int)field.GetValue(_socket.Options);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void SetBuffer_ArraySegment_Proxied()\n    {\n        // Arrange\n        int expected = 1234;\n        var buffer = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 });\n\n        // Act\n        _proxy.SetBuffer(expected, 1, buffer);\n\n        // Assert\n        var field = Me.GetField(\"_receiveBufferSize\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        int actual = (int)field.GetValue(_socket.Options);\n        Assert.Equal(expected, actual);\n\n        field = Me.GetField(\"_buffer\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(field);\n        ArraySegment<byte> segment = (ArraySegment<byte>)field.GetValue(_socket.Options);\n        Assert.Equal(buffer, segment);\n    }\n\n    [Fact]\n    public void SetRequestHeader_Proxied()\n    {\n        // Arrange\n        var expected = nameof(SetRequestHeader_Proxied);\n\n        // Act\n        _proxy.SetRequestHeader(\"test\", expected);\n\n        // Assert\n        var prop = Me.GetProperty(\"RequestHeaders\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(prop);\n        var actual = prop.GetValue(_socket.Options) as WebHeaderCollection;\n        Assert.NotNull(actual);\n        Assert.Single(actual);\n        Assert.Equal(nameof(SetRequestHeader_Proxied), actual.Get(\"test\"));\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/ClientWebSocketProxyTests.cs",
    "content": "﻿using Ocelot.WebSockets;\nusing System.Net.WebSockets;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\npublic sealed class ClientWebSocketProxyTests : UnitTest, IDisposable\n{\n    private readonly ClientWebSocketProxy _proxy;\n    private readonly Mock<WebSocket> _socket;\n    private readonly Mock<IClientWebSocketConnector> _connector;\n\n    public ClientWebSocketProxyTests()\n    {\n        _socket = new Mock<WebSocket>();\n        _connector = new Mock<IClientWebSocketConnector>();\n        _proxy = new(_socket.Object, _connector.Object);\n    }\n\n    public void Dispose() => _proxy.Dispose();\n\n    [Fact]\n    public void ToWebSocket_NoCasting()\n    {\n        // Arrange, Act\n        var actual = _proxy.ToWebSocket();\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.Equal(_socket.Object, actual);\n    }\n\n    [Fact]\n    public void Options_Proxied()\n    {\n        // Arrange\n        var options = new Mock<IClientWebSocketOptions>();\n        _connector.SetupGet(x => x.Options)\n            .Returns(options.Object).Verifiable();\n\n        // Act\n        var actual = _proxy.Options;\n\n        // Assert\n        Assert.NotNull(actual);\n        _connector.VerifyGet(x => x.Options, Times.Once());\n    }\n\n    [Fact]\n    public async Task ConnectAsync_Proxied()\n    {\n        // Arrange\n        var options = new Mock<IClientWebSocketOptions>();\n        _connector.Setup(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>()))\n            .Returns(Task.CompletedTask).Verifiable();\n\n        // Act\n        await _proxy.ConnectAsync(new(\"https://ocelot.net\"), CancellationToken.None);\n\n        // Assert\n        _connector.Verify(\n            x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public void CloseStatus_Proxied()\n    {\n        // Arrange\n        _socket.SetupGet(x => x.CloseStatus)\n            .Returns(WebSocketCloseStatus.Empty).Verifiable();\n\n        // Act\n        var actual = _proxy.CloseStatus;\n\n        // Assert\n        Assert.NotNull(actual);\n        _socket.VerifyGet(x => x.CloseStatus, Times.Once());\n    }\n\n    [Fact]\n    public void CloseStatusDescription_Proxied()\n    {\n        // Arrange\n        _socket.SetupGet(x => x.CloseStatusDescription)\n            .Returns(string.Empty).Verifiable();\n\n        // Act\n        var actual = _proxy.CloseStatusDescription;\n\n        // Assert\n        Assert.NotNull(actual);\n        _socket.VerifyGet(x => x.CloseStatusDescription, Times.Once());\n    }\n\n    [Fact]\n    public void State_Proxied()\n    {\n        // Arrange\n        _socket.SetupGet(x => x.State)\n            .Returns(WebSocketState.None).Verifiable();\n\n        // Act\n        var actual = _proxy.State;\n\n        // Assert\n        Assert.Equal(WebSocketState.None, actual);\n        _socket.VerifyGet(x => x.State, Times.Once());\n    }\n\n    [Fact]\n    public void SubProtocol_Proxied()\n    {\n        // Arrange\n        _socket.SetupGet(x => x.SubProtocol)\n            .Returns(Uri.UriSchemeWss).Verifiable();\n\n        // Act\n        var actual = _proxy.SubProtocol;\n\n        // Assert\n        Assert.Equal(Uri.UriSchemeWss, actual);\n        _socket.VerifyGet(x => x.SubProtocol, Times.Once());\n    }\n\n    [Fact]\n    public void Abort_Proxied()\n    {\n        // Arrange\n        _socket.Setup(x => x.Abort()).Verifiable();\n\n        // Act\n        _proxy.Abort();\n\n        // Assert\n        _socket.Verify(x => x.Abort(), Times.Once());\n    }\n\n    [Fact]\n    public async Task CloseAsync_Proxied()\n    {\n        // Arrange\n        _socket.Setup(x => x.CloseAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Verifiable();\n\n        // Act\n        await _proxy.CloseAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None);\n\n        // Assert\n        _socket.Verify(\n            x => x.CloseAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public async Task CloseOutputAsync_Proxied()\n    {\n        // Arrange\n        _socket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Verifiable();\n\n        // Act\n        await _proxy.CloseOutputAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None);\n\n        // Assert\n        _socket.Verify(\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public async Task ReceiveAsync_Proxied()\n    {\n        // Arrange\n        var expected = new WebSocketReceiveResult(123, WebSocketMessageType.Binary, true);\n        _socket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(expected).Verifiable();\n\n        // Act\n        var actual = await _proxy.ReceiveAsync(ArraySegment<byte>.Empty, CancellationToken.None);\n\n        // Assert\n        Assert.Equal(expected, actual);\n        _socket.Verify(\n            x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n    }\n\n    [Fact]\n    public async Task SendAsync_Proxied()\n    {\n        // Arrange\n        var expected = new WebSocketReceiveResult(123, WebSocketMessageType.Binary, true);\n        _socket.Setup(x => x.SendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))\n            .Returns(Task.CompletedTask).Verifiable();\n\n        // Act\n        await _proxy.SendAsync(ArraySegment<byte>.Empty, WebSocketMessageType.Binary, true, CancellationToken.None);\n\n        // Assert\n        _socket.Verify(\n            x => x.SendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/MockWebSocket.cs",
    "content": "﻿// Copyright © Kubernetes C# Client\n// Repository: https://github.com/kubernetes-client/csharp\n// Class: https://github.com/kubernetes-client/csharp/blob/master/tests/KubernetesClient.Tests/Mock/MockWebSocket.cs\n\nusing Nito.AsyncEx;\nusing System.Collections.Concurrent;\nusing System.Net.WebSockets;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\ninternal class MockWebSocket : WebSocket\n{\n    private WebSocketCloseStatus? closeStatus;\n    private string closeStatusDescription;\n    private WebSocketState state;\n    private readonly string subProtocol;\n    private readonly ConcurrentQueue<MessageData> receiveBuffers = new();\n    private readonly AsyncAutoResetEvent receiveEvent = new(false);\n    private bool disposedValue;\n\n    public MockWebSocket(string subProtocol = null) => this.subProtocol = subProtocol;\n    public void SetState(WebSocketState state) => this.state = state;\n    public EventHandler<MessageDataEventArgs> MessageSent { get; set; }\n\n    public Task InvokeReceiveAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage)\n    {\n        receiveBuffers.Enqueue(new MessageData()\n        {\n            Buffer = buffer,\n            MessageType = messageType,\n            EndOfMessage = endOfMessage,\n        });\n        receiveEvent.Set();\n        return Task.CompletedTask;\n    }\n\n    public override WebSocketCloseStatus? CloseStatus => closeStatus;\n    public override string CloseStatusDescription => closeStatusDescription;\n    public override WebSocketState State => state;\n    public override string SubProtocol => subProtocol;\n    public override void Abort() => throw new NotImplementedException();\n\n    public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription,\n        CancellationToken cancellationToken)\n    {\n        this.closeStatus = closeStatus;\n        closeStatusDescription = statusDescription;\n        receiveBuffers.Enqueue(new MessageData()\n        {\n            Buffer = new ArraySegment<byte>(Array.Empty<byte>()),\n            EndOfMessage = true,\n            MessageType = WebSocketMessageType.Close,\n        });\n        receiveEvent.Set();\n        return Task.CompletedTask;\n    }\n\n    public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => throw new NotImplementedException();\n\n    public override async Task<WebSocketReceiveResult> ReceiveAsync(\n        ArraySegment<byte> buffer,\n        CancellationToken cancellationToken)\n    {\n        if (receiveBuffers.IsEmpty)\n        {\n            await receiveEvent.WaitAsync(cancellationToken).ConfigureAwait(false);\n        }\n\n        var bytesReceived = 0;\n        var endOfMessage = true;\n        var messageType = WebSocketMessageType.Close;\n\n        if (receiveBuffers.TryPeek(out MessageData received))\n        {\n            messageType = received.MessageType;\n            if (received.Buffer.Count <= buffer.Count)\n            {\n                receiveBuffers.TryDequeue(out received);\n                received.Buffer.CopyTo(buffer);\n                bytesReceived = received.Buffer.Count;\n                endOfMessage = received.EndOfMessage;\n            }\n            else\n            {\n                received.Buffer.Slice(0, buffer.Count).CopyTo(buffer);\n                bytesReceived = buffer.Count;\n                endOfMessage = false;\n                received.Buffer = received.Buffer.Slice(buffer.Count);\n            }\n        }\n\n        return new WebSocketReceiveResult(bytesReceived, messageType, endOfMessage);\n    }\n\n    public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage,\n        CancellationToken cancellationToken)\n    {\n        MessageSent?.Invoke(\n            this,\n            new MessageDataEventArgs()\n            {\n                Data = new MessageData()\n                {\n                    Buffer = buffer,\n                    MessageType = messageType,\n                    EndOfMessage = endOfMessage,\n                },\n            });\n        return Task.CompletedTask;\n    }\n\n    public class MessageData\n    {\n        public ArraySegment<byte> Buffer { get; set; }\n        public WebSocketMessageType MessageType { get; set; }\n        public bool EndOfMessage { get; set; }\n    }\n\n    public class MessageDataEventArgs : EventArgs\n    {\n        public MessageData Data { get; set; }\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!disposedValue)\n        {\n            if (disposing)\n            {\n                receiveBuffers.Clear();\n                receiveEvent.Set();\n            }\n\n            disposedValue = true;\n        }\n    }\n\n    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources\n    // ~MockWebSocket()\n    // {\n    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n    //     Dispose(disposing: false);\n    // }\n    public override void Dispose()\n    {\n        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/WebSocketsFactoryTests.cs",
    "content": "﻿using Ocelot.WebSockets;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\npublic class WebSocketsFactoryTests\n{\n    [Fact]\n    public void CreateClient_Created()\n    {\n        // Arrange\n        WebSocketsFactory factory = new();\n\n        // Act\n        var actual = factory.CreateClient();\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<ClientWebSocketProxy>(actual);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Moq.Protected;\nusing Ocelot.Configuration;\nusing Ocelot.Configuration.Builder;\nusing Ocelot.Logging;\nusing Ocelot.Middleware;\nusing Ocelot.Request.Middleware;\nusing Ocelot.WebSockets;\nusing System.Linq.Expressions;\nusing System.Net.Security;\nusing System.Net.WebSockets;\nusing System.Reflection;\n\nnamespace Ocelot.UnitTests.WebSockets;\n\npublic class WebSocketsProxyMiddlewareTests : UnitTest\n{\n    private WebSocketsProxyMiddleware _middleware;\n\n    private readonly Mock<IOcelotLoggerFactory> _loggerFactory;\n    private readonly Mock<RequestDelegate> _next;\n    private readonly Mock<IWebSocketsFactory> _factory;\n\n    private readonly Mock<HttpContext> _context;\n    private readonly Mock<IOcelotLogger> _logger;\n    private readonly Mock<IClientWebSocket> _client;\n\n    public WebSocketsProxyMiddlewareTests()\n    {\n        _loggerFactory = new Mock<IOcelotLoggerFactory>();\n        _next = new Mock<RequestDelegate>();\n        _factory = new Mock<IWebSocketsFactory>();\n\n        _context = new Mock<HttpContext>();\n        _context.SetupGet(x => x.WebSockets.IsWebSocketRequest).Returns(true);\n\n        _logger = new Mock<IOcelotLogger>();\n        _loggerFactory.Setup(x => x.CreateLogger<WebSocketsProxyMiddleware>())\n            .Returns(_logger.Object);\n\n        _middleware = new WebSocketsProxyMiddleware(_loggerFactory.Object, _next.Object, _factory.Object);\n\n        _client = new Mock<IClientWebSocket>();\n        _factory.Setup(x => x.CreateClient()).Returns(_client.Object);\n    }\n\n    [Fact]\n    public async Task Proxy_NotIsWebSocketRequest_ThrownException()\n    {\n        // Arrange\n        List<object> messages = new();\n        GivenNonWebsocketScheme(Uri.UriSchemeHttps, messages);\n        _context.SetupGet(x => x.WebSockets.IsWebSocketRequest).Returns(false);\n\n        // Act\n        Task action() => _middleware.Invoke(_context.Object);\n\n        // Assert\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(action);\n        Assert.NotNull(ex);\n    }\n\n    [Fact]\n    public async Task Proxy_ThereAreWebSocketRequestedProtocols_AddedSubProtocols()\n    {\n        // Arrange\n        List<object> messages = new();\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndSetupProtocolsAndHeaders(\n            new() { Uri.UriSchemeHttps, Uri.UriSchemeWs, Uri.UriSchemeWss },\n            null);\n        AndDoNotConnectReally(null);\n        var options = new Mock<IClientWebSocketOptions>();\n        _client.SetupGet(x => x.Options)\n            .Returns(options.Object).Verifiable();\n        var actualProtos = new List<string>();\n        options.Setup(x => x.AddSubProtocol(It.IsAny<string>()))\n            .Callback<string>(actualProtos.Add).Verifiable();\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        _client.VerifyGet(x => x.Options, Times.Exactly(3));\n        options.Verify(x => x.AddSubProtocol(It.IsAny<string>()), Times.Exactly(3));\n        Assert.Equal(3, actualProtos.Count);\n    }\n\n    [Fact]\n    public async Task Proxy_ThereAreHeaders_SetRequestHeaders()\n    {\n        // Arrange\n        List<object> messages = new();\n        HeaderDictionary headers = new()\n        {\n            { \"TestMe\", nameof(Proxy_ThereAreHeaders_SetRequestHeaders) },\n        };\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndSetupProtocolsAndHeaders(null, headers);\n        AndDoNotConnectReally(null);\n        var options = new Mock<IClientWebSocketOptions>();\n        _client.SetupGet(x => x.Options).Returns(options.Object).Verifiable();\n        var actual = new Dictionary<string, string>();\n        options.Setup(x => x.SetRequestHeader(It.IsAny<string>(), It.IsAny<string>()))\n            .Callback<string, string>(actual.Add).Verifiable();\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        _client.VerifyGet(x => x.Options, Times.Exactly(1));\n        options.Verify(x => x.SetRequestHeader(It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(1));\n        Assert.Single(actual);\n        Assert.True(actual.ContainsKey(\"TestMe\"));\n        Assert.Equal(nameof(Proxy_ThereAreHeaders_SetRequestHeaders), actual[\"TestMe\"]);\n    }\n\n    [Fact]\n    public async Task Proxy_ThereAreHeaders_ThrownExceptionButCaughtIt()\n    {\n        // Arrange\n        List<object> messages = new();\n        HeaderDictionary headers = new()\n        {\n            { \"TestMe\", nameof(Proxy_ThereAreHeaders_ThrownExceptionButCaughtIt) },\n        };\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndSetupProtocolsAndHeaders(null, headers);\n        AndDoNotConnectReally(null);\n        var options = new Mock<IClientWebSocketOptions>();\n        _client.SetupGet(x => x.Options).Returns(options.Object).Verifiable();\n        var actual = new Dictionary<string, string>();\n        options.Setup(x => x.SetRequestHeader(It.IsAny<string>(), It.IsAny<string>()))\n            .Throws(new ArgumentException()); // !!!\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        _client.VerifyGet(x => x.Options, Times.Exactly(1));\n        options.Verify(x => x.SetRequestHeader(It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(1));\n        Assert.Empty(actual);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"1375 1237 925 920\")]\n    [Trait(\"PR\", \"1377\")] // https://github.com/ThreeMammals/Ocelot/pull/1377\n    public async Task ShouldIgnoreAllSslWarningsWhenDangerousAcceptAnyServerCertificateValidatorIsTrue()\n    {\n        // Arrange\n        List<object> actual = new();\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(true, actual);\n        AndDoNotSetupProtocolsAndHeaders();\n        AndDoNotConnectReally(null);\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        ThenIgnoredAllSslWarnings(actual);\n    }\n\n    private void GivenPropertyDangerousAcceptAnyServerCertificateValidator(bool enabled, List<object> messages)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Get,\n            new UriBuilder(Uri.UriSchemeWs, \"localhost\", PortFinder.GetRandomPort()).Uri);\n        var downstream = new DownstreamRequest(request);\n        var route = new DownstreamRouteBuilder()\n            .WithDangerousAcceptAnyServerCertificateValidator(enabled)\n            .Build();\n        _context.SetupGet(x => x.Items).Returns(new Dictionary<object, object>\n        {\n            { nameof(DownstreamRequest), downstream },\n            { nameof(DownstreamRoute), route },\n        });\n        _client.SetupSet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny<RemoteCertificateValidationCallback>())\n            .Callback<RemoteCertificateValidationCallback>(messages.Add);\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(y => messages.Add(y.Invoke()));\n    }\n\n    private void AndDoNotSetupProtocolsAndHeaders() => AndSetupProtocolsAndHeaders(null, null);\n    private void AndSetupProtocolsAndHeaders(List<string> protos = null, HeaderDictionary headers = null)\n    {\n        _context.SetupGet(x => x.WebSockets.WebSocketRequestedProtocols).Returns(protos ?? new());\n        _context.SetupGet(x => x.Request.Headers).Returns(headers ?? new());\n    }\n\n    private Mock<WebSocket> DoNotConnectReally(Action<Uri, CancellationToken> callbackConnectAsync, out Mock<WebSocket> server)\n    {\n        Action<Uri, CancellationToken> doNothing = (u, t) => { };\n        _client.Setup(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>()))\n            .Callback(callbackConnectAsync ?? doNothing);\n        var clientSocket = new Mock<WebSocket>();\n        var serverSocket = new Mock<WebSocket>();\n        _client.Setup(x => x.ToWebSocket()).Returns(clientSocket.Object);\n        _context.Setup(x => x.WebSockets.AcceptWebSocketAsync(It.IsAny<string>())).ReturnsAsync(serverSocket.Object);\n        server = serverSocket;\n        return clientSocket;\n    }\n\n    private void AndDoNotConnectReally(Action<Uri, CancellationToken> callbackConnectAsync)\n    {\n        var clientSocket = DoNotConnectReally(callbackConnectAsync, out var serverSocket);\n\n        var happyEnd = new WebSocketReceiveResult(1, WebSocketMessageType.Close, true);\n        clientSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(happyEnd);\n        serverSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(happyEnd);\n\n        clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()));\n        serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()));\n        clientSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty);\n        serverSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty);\n    }\n\n    private void ThenIgnoredAllSslWarnings(List<object> actual)\n    {\n        var route = _context.Object.Items.DownstreamRoute();\n        var request = _context.Object.Items.DownstreamRequest();\n        route.DangerousAcceptAnyServerCertificateValidator.ShouldBeTrue();\n\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var warning = actual.Last() as string;\n        warning.ShouldNotBeNullOrEmpty();\n        var expectedWarning = string.Format(WebSocketsProxyMiddleware.IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate);\n        warning.ShouldBe(expectedWarning);\n\n        _client.VerifySet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny<RemoteCertificateValidationCallback>(),\n            Times.Once());\n\n        var callback = actual.First() as RemoteCertificateValidationCallback;\n        callback.ShouldNotBeNull();\n        var validation = callback.Invoke(null, null, null, SslPolicyErrors.None);\n        validation.ShouldBeTrue();\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"1509 1683\")]\n    [Trait(\"PR\", \"1689\")] // https://github.com/ThreeMammals/Ocelot/pull/1689\n    [InlineData(\"http\", \"ws\")]\n    [InlineData(\"https\", \"wss\")]\n    [InlineData(\"ftp\", \"ftp\")]\n    public async Task ShouldReplaceNonWsSchemes(string scheme, string expectedScheme)\n    {\n        // Arrange\n        List<object> actual = new();\n        GivenNonWebsocketScheme(scheme, actual);\n        AndDoNotSetupProtocolsAndHeaders();\n        AndDoNotConnectReally((uri, token) => actual.Add(uri));\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        ThenNonWsSchemesAreReplaced(scheme, expectedScheme, actual);\n    }\n\n    private void GivenNonWebsocketScheme(string scheme, List<object> messages)\n    {\n        var requestMessage = new HttpRequestMessage(HttpMethod.Get, $\"{scheme}://localhost:12345\");\n        var request = new DownstreamRequest(requestMessage);\n        var route = new DownstreamRouteBuilder().Build();\n        var items = new Dictionary<object, object>\n        {\n            { nameof(DownstreamRequest), request },\n            { nameof(DownstreamRoute), route },\n        };\n        _context.SetupGet(x => x.Items).Returns(items);\n\n        _logger.Setup(x => x.LogWarning(It.IsAny<Func<string>>()))\n            .Callback<Func<string>>(myFunc => messages.Add(myFunc.Invoke()));\n    }\n\n    private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, List<object> actual)\n    {\n        var route = _context.Object.Items.DownstreamRoute();\n        var request = _context.Object.Items.DownstreamRequest();\n        route.DangerousAcceptAnyServerCertificateValidator.ShouldBeFalse();\n\n        _logger.Verify(x => x.LogWarning(It.IsAny<Func<string>>()), Times.Once());\n        var warning = actual.First() as string;\n        warning.ShouldNotBeNullOrEmpty();\n        warning.ShouldContain($\"'{scheme}'\");\n        var expectedWarning = string.Format(WebSocketsProxyMiddleware.InvalidSchemeWarningFormat, scheme, request.ToUri().Replace(expectedScheme, scheme));\n        warning.ShouldBe(expectedWarning);\n\n        request.Scheme.ShouldBe(expectedScheme);\n        ((Uri)actual.Last()).Scheme.ShouldBe(expectedScheme);\n    }\n\n    private static WebSocketCloseStatus[] AndBothSocketsGenerateExceptionWhenReceiveAsync(Mock<WebSocket> clientSocket, Mock<WebSocket> serverSocket, Exception error, Func<Task> closing)\n    {\n        var actual = new WebSocketCloseStatus[2];\n        var cresult = clientSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ThrowsAsync(error);\n        clientSocket.SetupGet(x => x.State).Returns(WebSocketState.Open);\n        clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns(closing)\n            .Callback<WebSocketCloseStatus, string, CancellationToken>((s, d, t) => actual[0] = s);\n\n        var sresult = serverSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ThrowsAsync(error);\n        serverSocket.SetupGet(x => x.State).Returns(WebSocketState.Open);\n        serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns(closing)\n            .Callback<WebSocketCloseStatus, string, CancellationToken>((s, d, t) => actual[1] = s);\n        return actual;\n    }\n\n    private static void ThenBothSocketsClosedOutputTimes(Mock<WebSocket> clientSocket, Mock<WebSocket> serverSocket, Times howMany)\n    {\n        clientSocket.Verify(\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            howMany);\n        serverSocket.Verify(\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            howMany);\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    public async Task PumpAsync_OperationCanceledException_ClosedDestinationSocket()\n    {\n        // Arrange\n        bool closed = false;\n        Task Closing()\n        {\n            closed = true;\n            return Task.CompletedTask;\n        }\n\n        var messages = new List<object>();\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndDoNotSetupProtocolsAndHeaders();\n        var clientSocket = DoNotConnectReally(null, out var serverSocket);\n        var error = new OperationCanceledException();\n        var actual = AndBothSocketsGenerateExceptionWhenReceiveAsync(clientSocket, serverSocket, error, Closing);\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        ThenBothSocketsClosedOutputTimes(clientSocket, serverSocket, Times.Once());\n        Assert.True(closed);\n        Assert.All(actual, s => Assert.Equal(WebSocketCloseStatus.EndpointUnavailable, s));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    public async Task PumpAsync_WebSocketException_ClosedDestinationSocket()\n    {\n        // Arrange\n        bool closed = false;\n        Task Closing()\n        {\n            closed = true;\n            return Task.CompletedTask;\n        }\n\n        var messages = new List<object>();\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndDoNotSetupProtocolsAndHeaders();\n        var clientSocket = DoNotConnectReally(null, out var serverSocket);\n        var error = new WebSocketException(WebSocketError.ConnectionClosedPrematurely);\n        var actual = AndBothSocketsGenerateExceptionWhenReceiveAsync(clientSocket, serverSocket, error, Closing);\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        ThenBothSocketsClosedOutputTimes(clientSocket, serverSocket, Times.Once());\n        Assert.True(closed);\n        Assert.All(actual, s => Assert.Equal(WebSocketCloseStatus.EndpointUnavailable, s));\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    public async Task PumpAsync_IsOpen_SentToDestination()\n    {\n        // Arrange\n        var messages = new List<object>();\n        GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages);\n        AndDoNotSetupProtocolsAndHeaders();\n        var clientSocket = DoNotConnectReally(null, out var serverSocket);\n\n        int clientCount = 0, serverCount = 0;\n        var open = new WebSocketReceiveResult(1, WebSocketMessageType.Binary, true);\n        var close = new WebSocketReceiveResult(1, WebSocketMessageType.Close, true);\n        clientSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(() => clientCount++ < 1 ? open : close);\n        serverSocket.Setup(x => x.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(() => serverCount++ < 1 ? open : close);\n        clientSocket.SetupGet(x => x.State).Returns(() => clientCount < 1 ? WebSocketState.Open : WebSocketState.Closed);\n        serverSocket.SetupGet(x => x.State).Returns(() => serverCount < 1 ? WebSocketState.Open : WebSocketState.Closed);\n        clientSocket.Setup(x => x.SendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()));\n        serverSocket.Setup(x => x.SendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()));\n        clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()));\n        serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()));\n        clientSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty);\n        serverSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty);\n        clientSocket.SetupGet(x => x.CloseStatusDescription).Returns(\"closed\");\n        serverSocket.SetupGet(x => x.CloseStatusDescription).Returns(\"closed\");\n\n        // Act\n        await _middleware.Invoke(_context.Object);\n\n        // Assert\n        Expression<Func<WebSocket, Task>> closeOutputAsync =\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>());\n        Expression<Func<WebSocket, Task>> sendAsync =\n            x => x.SendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>());\n        clientSocket.Verify(closeOutputAsync, Times.Never());\n        serverSocket.Verify(closeOutputAsync, Times.Once());\n        clientSocket.Verify(sendAsync, Times.Never());\n        serverSocket.Verify(sendAsync, Times.Once());\n        Assert.Equal(2, clientCount);\n        Assert.Equal(2, serverCount);\n    }\n\n    private static readonly Type Me = typeof(WebSocketsProxyMiddleware);\n    private Mock<WebSocketsProxyMiddleware> MockMiddleware()\n    {\n        static Task Next(HttpContext context) => Task.Delay(10);\n        RequestDelegate requestDelegate = Next;\n        var mock = new Mock<WebSocketsProxyMiddleware>(requestDelegate, _loggerFactory.Object, _factory.Object) { CallBase = true };\n        mock.Protected()\n            .Setup<Task>(\"TryCloseOutputAsync\", It.IsAny<WebSocket>(), It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>())\n            .Returns(Task.CompletedTask).Verifiable();\n\n        _middleware = mock.Object;\n        return mock;\n    }\n\n    [Fact]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    public async Task TryCloseOutputAsync_NoState_NoClosing()\n    {\n        // Arrange\n        //var mock = MockMiddleware();\n        var socket = new Mock<WebSocket>();\n        socket.SetupGet(x => x.State)\n            .Returns(WebSocketState.None);\n        socket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns(Task.CompletedTask).Verifiable();\n\n        // Act\n        var method = Me.GetMethod(\"TryCloseOutputAsync\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var actual = (Task)method.Invoke(_middleware, [ socket.Object, WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None ]);\n        await actual;\n\n        // Assert\n        Assert.True(actual.IsCompleted);\n        Assert.Equal(Task.CompletedTask, actual);\n        socket.Verify(\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            Times.Never());\n    }\n\n    [Theory]\n    [Trait(\"Bug\", \"930\")]\n    [Trait(\"PR\", \"2091\")] // https://github.com/ThreeMammals/Ocelot/pull/2091\n    [InlineData(WebSocketState.Open)]\n    [InlineData(WebSocketState.CloseReceived)]\n    public async Task TryCloseOutputAsync_MatchingState_HappyPath(WebSocketState state)\n    {\n        bool closed = false;\n        Task Closing()\n        {\n            closed = true;\n            return Task.CompletedTask;\n        }\n\n        // Arrange\n        var socket = new Mock<WebSocket>();\n        socket.SetupGet(x => x.State)\n            .Returns(state);\n        socket.Setup(x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns(Closing).Verifiable();\n\n        // Act\n        var method = Me.GetMethod(\"TryCloseOutputAsync\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var actual = (Task)method.Invoke(_middleware, [socket.Object, WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None]);\n        await actual;\n\n        // Assert\n        Assert.True(actual.IsCompleted);\n        Assert.Equal(Task.CompletedTask, actual);\n        socket.Verify(\n            x => x.CloseOutputAsync(It.IsAny<WebSocketCloseStatus>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),\n            Times.Once());\n        Assert.True(closed);\n    }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Trace\",\n      \"Microsoft.AspNetCore\": \"Trace\"\n    }\n  },\n  \"spring\": {\n    \"application\": {\n      \"name\": \"ocelot\"\n    }\n  },\n  \"eureka\": {\n    \"client\": {\n      \"serviceUrl\": \"http://localhost:8761/eureka/\",\n      \"shouldRegisterWithEureka\": true,\n      \"shouldFetchRegistry\": true,\n      \"port\": 5000,\n      \"hostName\": \"localhost\"\n    }\n  },\n  \"BaseUrl\": \"http://foo-bar.co.uk\",\n  \"TestConfig\": \"foo\",\n  \"TestConfigNested\":{\n    \"Child\": \"foo\"\n  }\n}\n"
  },
  {
    "path": "test/Ocelot.UnitTests/packages.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": {\n    \"net10.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==\"\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Microsoft.Reactive.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[7.0.0-preview.1, )\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==\",\n        \"dependencies\": {\n          \"System.Reactive\": \"7.0.0-preview.1\"\n        }\n      },\n      \"Nito.AsyncEx\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[5.1.2, )\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Context\": \"5.1.2\",\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\",\n          \"Nito.AsyncEx.Interop.WaitHandles\": \"5.1.2\",\n          \"Nito.AsyncEx.Oop\": \"5.1.2\",\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Cancellation\": \"1.1.2\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"10.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.0\",\n          \"Microsoft.Extensions.Http\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"10.0.5\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.0\",\n        \"contentHash\": \"r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging\": \"10.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.Extensions.Options\": \"10.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Nito.AsyncEx.Context\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Coordination\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Collections.Deque\": \"1.1.1\"\n        }\n      },\n      \"Nito.AsyncEx.Interop.WaitHandles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Oop\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Tasks\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Cancellation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.2\",\n        \"contentHash\": \"Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Collections.Deque\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.1\",\n        \"contentHash\": \"CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==\"\n      },\n      \"Nito.Disposables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.1\",\n        \"contentHash\": \"6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==\"\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[10.0.5, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[10.0.5, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\"\n        }\n      }\n    },\n    \"net10.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net10.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      }\n    },\n    \"net8.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.25, )\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Microsoft.Reactive.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[7.0.0-preview.1, )\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==\",\n        \"dependencies\": {\n          \"System.Reactive\": \"7.0.0-preview.1\"\n        }\n      },\n      \"Nito.AsyncEx\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[5.1.2, )\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Context\": \"5.1.2\",\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\",\n          \"Nito.AsyncEx.Interop.WaitHandles\": \"5.1.2\",\n          \"Nito.AsyncEx.Oop\": \"5.1.2\",\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Cancellation\": \"1.1.2\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"8.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.0\",\n          \"Microsoft.Extensions.Http\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.2\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.25\",\n        \"contentHash\": \"eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"8.0.25\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Logging\": \"7.1.2\",\n          \"Microsoft.IdentityModel.Tokens\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.1.2\",\n        \"contentHash\": \"6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"7.1.2\",\n          \"System.IdentityModel.Tokens.Jwt\": \"7.1.2\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Nito.AsyncEx.Context\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Coordination\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Collections.Deque\": \"1.1.1\"\n        }\n      },\n      \"Nito.AsyncEx.Interop.WaitHandles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Oop\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Tasks\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Cancellation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.2\",\n        \"contentHash\": \"Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Collections.Deque\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.1\",\n        \"contentHash\": \"CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==\"\n      },\n      \"Nito.Disposables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.1\",\n        \"contentHash\": \"6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==\"\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[8.0.25, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[8.0.25, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net8.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net8.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0\": {\n      \"Consul\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[1.7.14.10, )\",\n        \"resolved\": \"1.7.14.10\",\n        \"contentHash\": \"7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.1\"\n        }\n      },\n      \"coverlet.collector\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.0.0, )\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==\"\n      },\n      \"Microsoft.AspNetCore.TestHost\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[9.0.14, )\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==\"\n      },\n      \"Microsoft.Extensions.Caching.Memory\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.EnvironmentVariables\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.FileExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Json\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Console\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"System.Text.Json\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Debug\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Options.ConfigurationExtensions\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[10.0.5, )\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.NET.Test.Sdk\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[18.3.0, )\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==\",\n        \"dependencies\": {\n          \"Microsoft.CodeCoverage\": \"18.3.0\",\n          \"Microsoft.TestPlatform.TestHost\": \"18.3.0\"\n        }\n      },\n      \"Microsoft.Reactive.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[7.0.0-preview.1, )\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==\",\n        \"dependencies\": {\n          \"System.Reactive\": \"7.0.0-preview.1\"\n        }\n      },\n      \"Nito.AsyncEx\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[5.1.2, )\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Context\": \"5.1.2\",\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\",\n          \"Nito.AsyncEx.Interop.WaitHandles\": \"5.1.2\",\n          \"Nito.AsyncEx.Oop\": \"5.1.2\",\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Cancellation\": \"1.1.2\"\n        }\n      },\n      \"Polly\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"Polly.Testing\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.6.6, )\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==\",\n        \"dependencies\": {\n          \"Polly.Core\": \"8.6.6\"\n        }\n      },\n      \"System.IdentityModel.Tokens.Jwt\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[8.16.0, )\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.JsonWebTokens\": \"8.16.0\",\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"xunit.runner.visualstudio\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.1.5, )\",\n        \"resolved\": \"3.1.5\",\n        \"contentHash\": \"tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==\"\n      },\n      \"xunit.v3\": {\n        \"type\": \"Direct\",\n        \"requested\": \"[3.2.2, )\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==\",\n        \"dependencies\": {\n          \"xunit.v3.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"BouncyCastle.Cryptography\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.4.0\",\n        \"contentHash\": \"SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==\"\n      },\n      \"Castle.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.1\",\n        \"contentHash\": \"rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==\",\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"6.0.0\"\n        }\n      },\n      \"DiffEngine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"11.3.0\",\n        \"contentHash\": \"k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==\",\n        \"dependencies\": {\n          \"EmptyFiles\": \"4.4.0\",\n          \"System.Management\": \"6.0.1\"\n        }\n      },\n      \"EmptyFiles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.4.0\",\n        \"contentHash\": \"gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==\"\n      },\n      \"FluentValidation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"12.1.1\",\n        \"contentHash\": \"EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==\"\n      },\n      \"IPAddressRange\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.3.0\",\n        \"contentHash\": \"VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==\"\n      },\n      \"KubeClient\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"KubeClient.Http\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging\": \"9.0.3\"\n        }\n      },\n      \"KubeClient.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==\",\n        \"dependencies\": {\n          \"KubeClient\": \"3.1.1\",\n          \"KubeClient.Extensions.KubeConfig\": \"3.1.1\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"KubeClient.Extensions.KubeConfig\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==\",\n        \"dependencies\": {\n          \"BouncyCastle.Cryptography\": \"2.4.0\",\n          \"KubeClient\": \"3.1.1\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"System.Reactive\": \"6.0.1\",\n          \"YamlDotNet\": \"16.1.3\"\n        }\n      },\n      \"KubeClient.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.1\",\n        \"contentHash\": \"jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==\",\n        \"dependencies\": {\n          \"KubeClient.Core\": \"3.1.1\",\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.3\",\n          \"Microsoft.Extensions.Http\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.ApplicationInsights\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.23.0\",\n        \"contentHash\": \"nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==\"\n      },\n      \"Microsoft.AspNetCore.Authentication.JwtBearer\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.AspNetCore.JsonPatch\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.AspNetCore.MiddlewareAnalysis\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.14\"\n        }\n      },\n      \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.14\",\n        \"contentHash\": \"/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.JsonPatch\": \"9.0.14\",\n          \"Newtonsoft.Json\": \"13.0.3\",\n          \"Newtonsoft.Json.Bson\": \"1.0.2\"\n        }\n      },\n      \"Microsoft.Bcl.AsyncInterfaces\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==\"\n      },\n      \"Microsoft.CodeCoverage\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==\"\n      },\n      \"Microsoft.Extensions.Caching.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.Binder\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.CommandLine\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Configuration.UserSecrets\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==\"\n      },\n      \"Microsoft.Extensions.DiagnosticAdapter\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.1.32\",\n        \"contentHash\": \"oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==\"\n      },\n      \"Microsoft.Extensions.Diagnostics\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Diagnostics.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileProviders.Physical\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.FileSystemGlobbing\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.FileSystemGlobbing\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==\"\n      },\n      \"Microsoft.Extensions.Hosting\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.FileExtensions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Json\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.UserSecrets\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Physical\": \"8.0.0\",\n          \"Microsoft.Extensions.Hosting.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Debug\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventLog\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.EventSource\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Hosting.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Diagnostics.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.FileProviders.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"9.0.3\",\n        \"contentHash\": \"rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Diagnostics\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging\": \"9.0.3\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"9.0.3\",\n          \"Microsoft.Extensions.Options\": \"9.0.3\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"System.Diagnostics.DiagnosticSource\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.Configuration\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"10.0.5\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging\": \"10.0.5\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Options\": \"10.0.5\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"System.Diagnostics.EventLog\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Logging.EventSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Options\": \"8.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"8.0.0\"\n        }\n      },\n      \"Microsoft.Extensions.Options\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"10.0.5\",\n          \"Microsoft.Extensions.Primitives\": \"10.0.5\"\n        }\n      },\n      \"Microsoft.Extensions.Primitives\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==\"\n      },\n      \"Microsoft.IdentityModel.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==\"\n      },\n      \"Microsoft.IdentityModel.JsonWebTokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Logging\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Abstractions\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Tokens\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Protocols.OpenIdConnect\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.1\",\n        \"contentHash\": \"AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==\",\n        \"dependencies\": {\n          \"Microsoft.IdentityModel.Protocols\": \"8.0.1\",\n          \"System.IdentityModel.Tokens.Jwt\": \"8.0.1\"\n        }\n      },\n      \"Microsoft.IdentityModel.Tokens\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.16.0\",\n        \"contentHash\": \"rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Logging.Abstractions\": \"8.0.0\",\n          \"Microsoft.IdentityModel.Logging\": \"8.16.0\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.Telemetry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==\",\n        \"dependencies\": {\n          \"Microsoft.ApplicationInsights\": \"2.23.0\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.Testing.Platform\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==\"\n      },\n      \"Microsoft.Testing.Platform.MSBuild\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.9.1\",\n        \"contentHash\": \"oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Platform\": \"1.9.1\"\n        }\n      },\n      \"Microsoft.TestPlatform.ObjectModel\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==\"\n      },\n      \"Microsoft.TestPlatform.TestHost\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"18.3.0\",\n        \"contentHash\": \"twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==\",\n        \"dependencies\": {\n          \"Microsoft.TestPlatform.ObjectModel\": \"18.3.0\",\n          \"Newtonsoft.Json\": \"13.0.3\"\n        }\n      },\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"Moq\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.20.72\",\n        \"contentHash\": \"EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==\",\n        \"dependencies\": {\n          \"Castle.Core\": \"5.1.1\"\n        }\n      },\n      \"Newtonsoft.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"13.0.3\",\n        \"contentHash\": \"HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==\"\n      },\n      \"Newtonsoft.Json.Bson\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.0.2\",\n        \"contentHash\": \"QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==\",\n        \"dependencies\": {\n          \"Newtonsoft.Json\": \"12.0.1\"\n        }\n      },\n      \"Nito.AsyncEx.Context\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Coordination\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\",\n          \"Nito.Collections.Deque\": \"1.1.1\"\n        }\n      },\n      \"Nito.AsyncEx.Interop.WaitHandles\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Tasks\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Oop\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==\",\n        \"dependencies\": {\n          \"Nito.AsyncEx.Coordination\": \"5.1.2\"\n        }\n      },\n      \"Nito.AsyncEx.Tasks\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.1.2\",\n        \"contentHash\": \"jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Cancellation\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.2\",\n        \"contentHash\": \"Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==\",\n        \"dependencies\": {\n          \"Nito.Disposables\": \"2.2.1\"\n        }\n      },\n      \"Nito.Collections.Deque\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.1.1\",\n        \"contentHash\": \"CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==\"\n      },\n      \"Nito.Disposables\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"2.2.1\",\n        \"contentHash\": \"6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==\"\n      },\n      \"Polly.Core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.6.6\",\n        \"contentHash\": \"lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==\"\n      },\n      \"Shouldly\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.3.0\",\n        \"contentHash\": \"sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==\",\n        \"dependencies\": {\n          \"DiffEngine\": \"11.3.0\",\n          \"EmptyFiles\": \"4.4.0\"\n        }\n      },\n      \"Steeltoe.Common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Caching.Abstractions\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.CommandLine\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.EnvironmentVariables\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Microsoft.Extensions.Logging.Console\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"System.Reflection.MetadataLoadContext\": \"4.6.0\"\n        }\n      },\n      \"Steeltoe.Common.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\"\n        }\n      },\n      \"Steeltoe.Common.Http\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Http\": \"3.1.0\",\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Extensions.Configuration.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Connector.ConnectorBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==\",\n        \"dependencies\": {\n          \"Steeltoe.Common\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration.Abstractions\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientBase\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Hosting\": \"8.0.0\",\n          \"Microsoft.Extensions.Options.ConfigurationExtensions\": \"8.0.0\",\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.ConnectorBase\": \"3.3.0\",\n          \"Steeltoe.Discovery.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.ClientCore\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==\",\n        \"dependencies\": {\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Discovery.Eureka\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==\",\n        \"dependencies\": {\n          \"Steeltoe.Common.Http\": \"3.3.0\",\n          \"Steeltoe.Connector.Abstractions\": \"3.3.0\",\n          \"Steeltoe.Discovery.ClientBase\": \"3.3.0\"\n        }\n      },\n      \"Steeltoe.Extensions.Configuration.Abstractions\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.3.0\",\n        \"contentHash\": \"8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==\",\n        \"dependencies\": {\n          \"Microsoft.Extensions.Configuration\": \"8.0.0\",\n          \"Microsoft.Extensions.Configuration.Binder\": \"8.0.0\",\n          \"Microsoft.Extensions.DependencyInjection\": \"8.0.0\",\n          \"Steeltoe.Common.Abstractions\": \"3.3.0\"\n        }\n      },\n      \"System.CodeDom\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.0\",\n        \"contentHash\": \"CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==\"\n      },\n      \"System.Diagnostics.DiagnosticSource\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.IO.Pipelines\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Reactive\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"7.0.0-preview.1\",\n        \"contentHash\": \"eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==\"\n      },\n      \"System.Reflection.MetadataLoadContext\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"4.6.0\",\n        \"contentHash\": \"TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==\"\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      },\n      \"System.Text.Json\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==\",\n        \"dependencies\": {\n          \"System.IO.Pipelines\": \"10.0.5\",\n          \"System.Text.Encodings.Web\": \"10.0.5\"\n        }\n      },\n      \"xunit.analyzers\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"1.27.0\",\n        \"contentHash\": \"y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==\"\n      },\n      \"xunit.v3.assert\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==\"\n      },\n      \"xunit.v3.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==\",\n        \"dependencies\": {\n          \"Microsoft.Bcl.AsyncInterfaces\": \"6.0.0\"\n        }\n      },\n      \"xunit.v3.core.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==\",\n        \"dependencies\": {\n          \"Microsoft.Testing.Extensions.Telemetry\": \"1.9.1\",\n          \"Microsoft.Testing.Extensions.TrxReport.Abstractions\": \"1.9.1\",\n          \"Microsoft.Testing.Platform\": \"1.9.1\",\n          \"Microsoft.Testing.Platform.MSBuild\": \"1.9.1\",\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.inproc.console\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.extensibility.core\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==\",\n        \"dependencies\": {\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.mtp-v1\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==\",\n        \"dependencies\": {\n          \"xunit.analyzers\": \"1.27.0\",\n          \"xunit.v3.assert\": \"[3.2.2]\",\n          \"xunit.v3.core.mtp-v1\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.common\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==\",\n        \"dependencies\": {\n          \"Microsoft.Win32.Registry\": \"[5.0.0]\",\n          \"xunit.v3.common\": \"[3.2.2]\"\n        }\n      },\n      \"xunit.v3.runner.inproc.console\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"3.2.2\",\n        \"contentHash\": \"ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==\",\n        \"dependencies\": {\n          \"xunit.v3.extensibility.core\": \"[3.2.2]\",\n          \"xunit.v3.runner.common\": \"[3.2.2]\"\n        }\n      },\n      \"YamlDotNet\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"16.1.3\",\n        \"contentHash\": \"gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==\"\n      },\n      \"ocelot\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"FluentValidation\": \"[12.1.1, )\",\n          \"IPAddressRange\": \"[6.3.0, )\",\n          \"Microsoft.AspNetCore.MiddlewareAnalysis\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.Extensions.DiagnosticAdapter\": \"[3.1.32, )\"\n        }\n      },\n      \"ocelot.provider.consul\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Consul\": \"[1.7.14.10, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.eureka\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Steeltoe.Discovery.ClientCore\": \"[3.3.0, )\",\n          \"Steeltoe.Discovery.Eureka\": \"[3.3.0, )\"\n        }\n      },\n      \"ocelot.provider.kubernetes\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"KubeClient\": \"[3.1.1, )\",\n          \"KubeClient.Extensions.DependencyInjection\": \"[3.1.1, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\"\n        }\n      },\n      \"ocelot.provider.polly\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Polly\": \"[8.6.6, )\"\n        }\n      },\n      \"ocelot.testing\": {\n        \"type\": \"Project\",\n        \"dependencies\": {\n          \"Microsoft.AspNetCore.Authentication.JwtBearer\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.Mvc.NewtonsoftJson\": \"[9.0.14, )\",\n          \"Microsoft.AspNetCore.TestHost\": \"[9.0.14, )\",\n          \"Moq\": \"[4.20.72, )\",\n          \"Ocelot\": \"[0.0.0-dev, )\",\n          \"Shouldly\": \"[4.3.0, )\",\n          \"System.Text.Json\": \"[10.0.5, )\"\n        }\n      }\n    },\n    \"net9.0/osx-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    },\n    \"net9.0/win-x64\": {\n      \"Microsoft.Win32.Registry\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"5.0.0\",\n        \"contentHash\": \"dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\"\n      },\n      \"System.Diagnostics.EventLog\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"8.0.0\",\n        \"contentHash\": \"fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==\"\n      },\n      \"System.Management\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"6.0.1\",\n        \"contentHash\": \"10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==\",\n        \"dependencies\": {\n          \"System.CodeDom\": \"6.0.0\"\n        }\n      },\n      \"System.Text.Encodings.Web\": {\n        \"type\": \"Transitive\",\n        \"resolved\": \"10.0.5\",\n        \"contentHash\": \"opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "testing/AcceptanceSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.TestHost;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Ocelot.Middleware;\nusing Shouldly;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http.Headers;\nusing System.Runtime.CompilerServices;\nusing System.Text.Encodings.Web;\nusing System.Text.Json;\nusing System.Text.Unicode;\nusing CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue;\nusing MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;\n\nnamespace Ocelot.Testing;\n\n/// <summary>\n/// This is the base class for acceptance testing classes, specifically for developing Step classes.\n/// It is the recommended base class to inherit from.\n/// </summary>\npublic class AcceptanceSteps : IDisposable\n{\n    protected IHost? ocelotHost;\n    protected TestServer? ocelotServer;\n    protected HttpClient? ocelotClient;\n    protected HttpResponseMessage? response;\n\n    private readonly Guid _testId;\n    protected readonly Random random;\n    protected readonly string ocelotConfigFileName;\n    protected readonly ServiceHandler handler;\n\n    public AcceptanceSteps()\n    {\n        _testId = Guid.NewGuid();\n        random = new Random();\n        ocelotConfigFileName = $\"ocelot-{_testId:N}.json\"; // {ConfigurationBuilderExtensions.PrimaryConfigFile}\";\n        Files = [ocelotConfigFileName];\n        Folders = [];\n        handler = new();\n    }\n\n    protected List<string> Files { get; }\n    protected List<string> Folders { get; }\n    protected virtual string TestID { get => _testId.ToString(\"N\"); }\n    public virtual string Body([CallerMemberName] string? responseBody = null) => responseBody ?? GetType().Name;\n    public virtual string TestName([CallerMemberName] string? testName = null) => testName ?? GetType().Name; // but it could be TestID also\n\n    public HttpClient? OcelotClient => ocelotClient;\n\n    protected virtual FileHostAndPort Localhost(int port) => new(\"localhost\", port);\n\n    protected static string DownstreamUrl(int port) => DownstreamUrl(port, Uri.UriSchemeHttp);\n    protected static string DownstreamUrl(int port, string scheme) => $\"{scheme ?? Uri.UriSchemeHttp}://localhost:{port}\";\n    protected static string LoopbackLocalhostUrl(int port, int loopbackIndex = 0) => $\"{Uri.UriSchemeHttp}://127.0.0.{++loopbackIndex}:{port}\";\n\n    public virtual FileConfiguration GivenConfiguration(params FileRoute[] routes)\n    {\n        var c = new FileConfiguration();\n        c.Routes.AddRange(routes);\n        return c;\n    }\n\n    public virtual FileRoute GivenDefaultRoute(int port) => GivenRoute(port);\n    public virtual FileRoute GivenCatchAllRoute(int port) => GivenRoute(port, \"/{everything}\", \"/{everything}\");\n    public virtual FileRoute GivenRoute(int port, string? upstream = null, string? downstream = null)\n    {\n        var r = new FileRoute();\n        r.DownstreamHostAndPorts.Add(Localhost(port));\n        r.DownstreamPathTemplate = downstream ?? \"/\";\n        r.DownstreamScheme = Uri.UriSchemeHttp;\n        r.UpstreamHttpMethod.Add(HttpMethods.Get);\n        r.UpstreamPathTemplate = upstream ?? \"/\";\n        return r;\n    }\n\n    public virtual void GivenThereIsAConfiguration(FileConfiguration configuration)\n        => GivenThereIsAConfiguration(configuration, ocelotConfigFileName);\n    public virtual void GivenThereIsAConfiguration(FileConfiguration from, string toFile)\n    {\n        var json = SerializeJson(from, ref toFile);\n        File.WriteAllText(toFile, json);\n    }\n    public virtual Task GivenThereIsAConfigurationAsync(FileConfiguration from, string toFile)\n    {\n        var json = SerializeJson(from, ref toFile);\n        return File.WriteAllTextAsync(toFile, json);\n    }\n    protected virtual string SerializeJson(FileConfiguration from, ref string toFile)\n    {\n        toFile ??= ocelotConfigFileName;\n        Files.Add(toFile); // register for disposing\n        return JsonSerializer.Serialize(from, JsonWebIndented /*Formatting.Indented*/);\n    }\n\n    public readonly static JsonSerializerOptions JsonWebIndented = new()\n    {\n        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), // Avoid escaping non-ASCII\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,     // Use camelCase for web\n        WriteIndented = true,                                  // Not compact output for better readability\n        PropertyNameCaseInsensitive = true                     // Optional: for deserialization\n    };\n\n    #region GivenOcelotIsRunning\n    public void WithBasicConfiguration(HostBuilderContext hosting, IConfigurationBuilder config)\n    {\n        config.SetBasePath(hosting.HostingEnvironment.ContentRootPath);\n        config.AddOcelot(ocelotConfigFileName, false, false);\n    }\n    public void WithBasicConfiguration(WebHostBuilderContext hosting, IConfigurationBuilder config)\n    {\n        config.SetBasePath(hosting.HostingEnvironment.ContentRootPath);\n        config.AddOcelot(ocelotConfigFileName, false, false);\n    }\n\n    public static void WithAddOcelot(IServiceCollection services) => services.AddOcelot();\n    public static void WithUseOcelot(IApplicationBuilder app) => WithUseOcelotAsync(app).GetAwaiter().GetResult();\n    public static Task<IApplicationBuilder> WithUseOcelotAsync(IApplicationBuilder app) => app.UseOcelot();\n\n    public int GivenOcelotIsRunning()\n        => GivenOcelotIsRunning(null, null, null, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync()\n        => GivenOcelotIsRunningAsync(null, null, null, null, null, null, null);\n\n    public int GivenOcelotIsRunning(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)\n        => GivenOcelotIsRunning(configureDelegate, null, null, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)\n        => GivenOcelotIsRunningAsync(configureDelegate, null, null, null, null, null, null);\n\n    public int GivenOcelotIsRunning(Action<IServiceCollection> configureServices)\n        => GivenOcelotIsRunning(null, configureServices, null, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync(Action<IServiceCollection> configureServices)\n        => GivenOcelotIsRunningAsync(null, configureServices, null, null, null, null, null);\n\n    public int GivenOcelotIsRunning(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate, Action<IServiceCollection> configureServices)\n        => GivenOcelotIsRunning(configureDelegate, configureServices, null, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate, Action<IServiceCollection> configureServices)\n        => GivenOcelotIsRunningAsync(configureDelegate, configureServices, null, null, null, null, null);\n\n    public int GivenOcelotIsRunning(Action<IApplicationBuilder>? configureApp)\n        => GivenOcelotIsRunning(null, null, configureApp, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync(Action<IApplicationBuilder>? configureApp)\n        => GivenOcelotIsRunningAsync(null, null, configureApp, null, null, null, null);\n\n    public int GivenOcelotIsRunning(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate, Action<IServiceCollection> configureServices, Action<IApplicationBuilder>? configureApp)\n        => GivenOcelotIsRunning(configureDelegate, configureServices, configureApp, null, null, null, null);\n    public Task<int> GivenOcelotIsRunningAsync(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate, Action<IServiceCollection> configureServices, Action<IApplicationBuilder>? configureApp)\n        => GivenOcelotIsRunningAsync(configureDelegate, configureServices, configureApp, null, null, null, null);\n\n    protected int GivenOcelotIsRunning(\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost, Action<IWebHostBuilder>? postConfigureHost,\n        Action<TestServer>? configureServer,\n        Action<HttpClient>? configureClient)\n    {\n#if NET10_0_OR_GREATER\n        return GivenOcelotHostIsRunning(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient)\n            .GetAwaiter().GetResult();\n#else\n        return GivenOcelotIsRunningInternal(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient);\n#endif\n    }\n\n    protected Task<int> GivenOcelotIsRunningAsync(\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost, Action<IWebHostBuilder>? postConfigureHost,\n        Action<TestServer>? configureServer,\n        Action<HttpClient>? configureClient)\n    {\n#if NET10_0_OR_GREATER\n        return GivenOcelotHostIsRunning(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient);\n#else\n        return Task.Run(() => GivenOcelotIsRunningInternal(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient));\n#endif\n    }\n\n#if (NET8_0 || NET9_0)\n    private int GivenOcelotIsRunningInternal(\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? сonfigureWebHost, Action<IWebHostBuilder>? postConfigureHost,\n        Action<TestServer>? configureServer,\n        Action<HttpClient>? configureClient)\n    {\n        int port = PortFinder.GetRandomPort();\n        var baseUrl = DownstreamUrl(port);\n        var builder = TestHostBuilder.Create();\n        if (сonfigureWebHost is not null)\n            сonfigureWebHost(builder);\n        else builder\n            .ConfigureAppConfiguration(configureDelegate ?? WithBasicConfiguration)\n            .ConfigureServices(configureServices ?? WithAddOcelot)\n            .Configure(configureApp ?? WithUseOcelot)\n            .UseUrls(baseUrl); // run Ocelot on specific port, rather than on std 80 port of TestServer\n        postConfigureHost?.Invoke(builder);\n\n        ocelotServer = new(builder)\n        {\n            BaseAddress = new(baseUrl) // will create Oc client with this base address, including port\n        };\n        configureServer?.Invoke(ocelotServer);\n        ocelotClient = ocelotServer.CreateClient();\n        configureClient?.Invoke(ocelotClient);\n        return port;\n    }\n#endif\n    private static void SetBaseUrl(FileConfiguration configuration, string baseUrl)\n    {\n        configuration.GlobalConfiguration.BaseUrl = baseUrl;\n    }\n\n    protected async Task<int> GivenOcelotHostIsRunning(\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? сonfigureWebHost, Action<IWebHostBuilder>? postConfigureHost,\n        Action<TestServer>? configureServer,\n        Action<HttpClient>? configureClient)\n    {\n        int port = PortFinder.GetRandomPort();\n        var baseUrl = DownstreamUrl(port);\n        void ConfigureWeb(IWebHostBuilder builder)\n        {\n            builder\n                .UseKestrel()\n                .ConfigureAppConfiguration(configureDelegate ?? WithBasicConfiguration)\n                .ConfigureServices(configureServices ?? WithAddOcelot)\n                .Configure(configureApp ?? WithUseOcelot)\n                .UseUrls(baseUrl);\n                //.UseTestServer(o => o.BaseAddress = new(baseUrl));\n            postConfigureHost?.Invoke(builder);\n        }\n        var host = TestHostBuilder\n            .CreateHost()\n            .ConfigureWebHost(сonfigureWebHost ?? ConfigureWeb)\n            .Build();\n        await host.StartAsync();\n        ocelotHost = host;\n        //ocelotServer = host.GetTestServer();\n        //configureServer?.Invoke(ocelotServer!);\n        ocelotClient = ocelotServer?.CreateClient();\n        ocelotClient ??= new() { BaseAddress = new(baseUrl), };\n        configureClient?.Invoke(ocelotClient);\n        return port;\n    }\n\n    protected IServiceProvider OcelotServices { get => ocelotServer?.Services ?? ocelotHost!.Services; }\n    #endregion\n\n    #region GivenThereIsAServiceRunningOn\n\n    public virtual void GivenThereIsAServiceRunningOn(int port, [CallerMemberName] string responseBody = \"\")\n        => GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, responseBody);\n\n    protected virtual HttpStatusCode MapStatus_StatusCode { get; set; } = HttpStatusCode.OK;\n    protected virtual Func<HttpContext, string>? MapStatus_ResponseBody { get; set; }\n    protected virtual Task MapStatus(HttpContext context)\n    {\n        context.Response.StatusCode = (int)MapStatus_StatusCode;\n        return context.Response.WriteAsync(MapStatus_ResponseBody?.Invoke(context) ?? string.Empty);\n    }\n    public virtual void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, [CallerMemberName] string responseBody = \"\")\n    {\n        MapStatus_StatusCode = statusCode;\n        MapStatus_ResponseBody ??= (ctx) => responseBody;\n        handler.GivenThereIsAServiceRunningOn(port, MapStatus);\n    }\n\n    protected virtual Task MapOK(HttpContext context)\n    {\n        context.Response.StatusCode = StatusCodes.Status200OK;\n        return context.Response.WriteAsync(MapStatus_ResponseBody?.Invoke(context) ?? string.Empty);\n    }\n    public virtual void GivenThereIsAServiceRunningOnPath(int port, string basePath, [CallerMemberName] string responseBody = \"\")\n    {\n        MapStatus_ResponseBody ??= (ctx) => responseBody;\n        handler.GivenThereIsAServiceRunningOn(port, basePath, MapOK);\n    }\n    public virtual void GivenThereIsAServiceRunningOn(int port, string basePath, RequestDelegate requestDelegate)\n    {\n        handler.GivenThereIsAServiceRunningOn(port, basePath, requestDelegate);\n    }\n    #endregion\n\n    public static void GivenIWait(int wait) => Thread.Sleep(wait);\n    public static Task GivenIWaitAsync(int wait) => Task.Delay(wait);\n\n    #region Cookies\n\n    public void GivenIAddCookieToMyRequest(string cookie)\n        => ocelotClient.ShouldNotBeNull().DefaultRequestHeaders.Add(\"Set-Cookie\", cookie);\n    public async Task WhenIGetUrlOnTheApiGatewayWithCookie(string url, string cookie, string value)\n        => response = await WhenIGetUrlOnTheApiGateway(url, cookie, value);\n    public async Task WhenIGetUrlOnTheApiGatewayWithCookie(string url, CookieHeaderValue cookie)\n        => response = await WhenIGetUrlOnTheApiGateway(url, cookie);\n    public Task<HttpResponseMessage> WhenIGetUrlOnTheApiGateway(string url, string cookie, string value)\n        => WhenIGetUrlOnTheApiGateway(url, new CookieHeaderValue(cookie, value));\n    public Task<HttpResponseMessage> WhenIGetUrlOnTheApiGateway(string url, CookieHeaderValue cookie)\n    {\n        var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);\n        requestMessage.Headers.Add(\"Cookie\", cookie.ToString());\n        return ocelotClient.ShouldNotBeNull().SendAsync(requestMessage);\n    }\n    #endregion\n\n    #region Headers\n\n    public void ThenTheResponseHeaderIs(string key, string value)\n    {\n        ThenTheResponseHeaderExists(key);\n        var header = response?.Headers.GetValues(key) ?? [];\n        header.Any(string.IsNullOrEmpty).ShouldBeFalse();\n        string.Join(';', header).ShouldBe(value);\n    }\n\n    public void ThenTheResponseContentHeaderIs(string key, string value)\n    {\n        ThenTheResponseContentHeaderExists(key);\n        var header = response?.Content.Headers.GetValues(key) ?? [];\n        header.Any(string.IsNullOrEmpty).ShouldBeFalse();\n        string.Join(';', header).ShouldBe(value);\n    }\n\n    public string ThenTheResponseHeaderExists(string key)\n    {\n        response.ShouldNotBeNull().Headers.Contains(key).ShouldBeTrue();\n        var header = response.Headers.GetValues(key);\n        return string.Join(';', header);\n    }\n\n    public void ThenTheResponseHeaderExists(string key, bool exists)\n        => response.ShouldNotBeNull().Headers.Contains(key).ShouldBe(exists);\n\n    public string ThenTheResponseContentHeaderExists(string key)\n    {\n        response.ShouldNotBeNull().Content.Headers.Contains(key).ShouldBeTrue();\n        var header = response.Content.Headers.GetValues(key);\n        return string.Join(';', header);\n    }\n\n    public void ThenTheResponseContentHeaderExists(string key, bool exists)\n        => response.ShouldNotBeNull().Content.Headers.Contains(key).ShouldBe(exists);\n    #endregion\n\n    public void ThenTheResponseReasonPhraseIs(string expected)\n        => response.ShouldNotBeNull().ReasonPhrase.ShouldBe(expected);\n\n    public void GivenIHaveAddedATokenToMyRequest(string token, string scheme = \"Bearer\")\n    {\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme, token);\n    }\n\n    public async Task WhenIGetUrlOnTheApiGateway(string url)\n        => response = await ocelotClient.ShouldNotBeNull().GetAsync(url);\n\n    public Task<HttpResponseMessage> WhenIGetUrl(string url)\n        => ocelotClient.ShouldNotBeNull().GetAsync(url);\n\n    public async Task WhenIGetUrlOnTheApiGatewayWithBody(string url, string body)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Get, url)\n        {\n            Content = new StringContent(body),\n        };\n        response = await ocelotClient.ShouldNotBeNull().SendAsync(request);\n    }\n\n    public async Task WhenIGetUrlOnTheApiGatewayWithForm(string url, string name, IEnumerable<KeyValuePair<string, string>> values)\n    {\n        var content = new MultipartFormDataContent();\n        var dataContent = new FormUrlEncodedContent(values);\n        content.Add(dataContent, name);\n        content.Headers.ContentDisposition = new ContentDispositionHeaderValue(\"form-data\");\n        var request = new HttpRequestMessage(HttpMethod.Get, url)\n        {\n            Content = content,\n        };\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        response = await ocelotClient.SendAsync(request);\n    }\n\n    public async Task WhenIGetUrlOnTheApiGateway(string url, HttpContent content)\n    {\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content };\n        response = await ocelotClient.SendAsync(httpRequestMessage);\n    }\n\n    public async Task WhenIPostUrlOnTheApiGateway(string url, HttpContent content)\n    {\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };\n        response = await ocelotClient.SendAsync(request);\n    }\n    public async Task WhenIPostUrlOnTheApiGateway(string url, string content)\n    {\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        var postContent = new StringContent(content);\n        response = await ocelotClient.PostAsync(url, postContent);\n    }\n    public async Task WhenIPostUrlOnTheApiGateway(string url, string content, string contentType)\n    {\n        ArgumentNullException.ThrowIfNull(ocelotClient);\n        var postContent = new StringContent(content, new MediaTypeHeaderValue(contentType));\n        response = await ocelotClient.PostAsync(url, postContent);\n    }\n    public async Task WhenIDeleteUrlOnTheApiGateway(string url)\n        => response = await ocelotClient.ShouldNotBeNull().DeleteAsync(url);\n\n    public void GivenIAddAHeader(string key, string value)\n    {\n        key.ShouldNotBeNullOrEmpty();\n        value.ShouldNotBeNullOrEmpty();\n        ocelotClient.ShouldNotBeNull().DefaultRequestHeaders.TryAddWithoutValidation(key, value);\n    }\n\n    public static void WhenIDoActionMultipleTimes(int times, Action<int> action)\n    {\n        for (int i = 0; i < times; i++)\n            action?.Invoke(i);\n    }\n\n    public static async Task WhenIDoActionMultipleTimes(int times, Func<int, Task> action)\n    {\n        for (int i = 0; i < times; i++)\n            await action.Invoke(i);\n    }\n    public static async Task WhenIDoActionForTime(TimeSpan time, Func<int, Task> action)\n    {\n        var watcher = Stopwatch.StartNew();\n        for (int i = 0; watcher.Elapsed < time; i++)\n        {\n            await action.Invoke(i);\n        }\n        watcher.Stop();\n    }\n\n    public void ThenTheResponseBody([CallerMemberName] string testName = \"\")\n        => ThenTheResponseBodyShouldBe(testName);\n    public Task ThenTheResponseBodyAsync([CallerMemberName] string testName = \"\")\n        => ThenTheResponseBodyShouldBeAsync(testName);\n\n    public void ThenTheResponseBodyShouldBe(string expectedBody)\n        => response.ShouldNotBeNull()\n        .Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody);\n    public Task ThenTheResponseBodyShouldBeAsync(string expectedBody)\n        => response.ShouldNotBeNull()\n        .Content.ReadAsStringAsync()\n        .ContinueWith(t => t.Result.ShouldBe(expectedBody));\n\n    public void ThenTheResponseBodyShouldBe(string expectedBody, string customMessage)\n        => response.ShouldNotBeNull()\n        .Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody, customMessage);\n    public Task ThenTheResponseBodyShouldBeAsync(string expectedBody, string customMessage)\n        => response.ShouldNotBeNull()\n        .Content.ReadAsStringAsync()\n        .ContinueWith(t => t.Result.ShouldBe(expectedBody, customMessage));\n\n    public Task ThenTheResponseShouldBeAsync(HttpStatusCode expected, [CallerMemberName] string? expectedBody = null)\n    {\n        ThenTheStatusCodeShouldBe(expected);\n        return ThenTheResponseBodyShouldBeAsync(expectedBody ?? Body(expectedBody));\n    }\n    public Task ThenTheResponseBodyShouldBeEmpty() => ThenTheResponseBodyShouldBeAsync(string.Empty);\n\n    public void ThenTheContentLengthIs(int expected)\n        => response.ShouldNotBeNull().Content.Headers.ContentLength.ShouldBe(expected);\n\n    public void ThenTheStatusCodeShouldBeOK()\n        => ThenTheStatusCodeShouldBe(HttpStatusCode.OK);\n    public void ThenTheStatusCodeShouldBe(HttpStatusCode expected)\n        => response.ShouldNotBeNull().StatusCode.ShouldBe(expected);\n    public void ThenTheStatusCodeShouldBe(int expected)\n        => ((int)response.ShouldNotBeNull().StatusCode).ShouldBe(expected);\n\n    public Task ReleasePortAsync(params int[] ports)\n        => handler.ReleasePortAsync(ports);\n\n    #region Dispose pattern\n\n    /// <summary>\n    /// Public implementation of Dispose pattern callable by consumers.\n    /// </summary>\n    public virtual void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    private bool _disposedValue;\n\n    /// <summary>Protected implementation of Dispose pattern.</summary>\n    /// <param name=\"disposing\">Flag to trigger actual disposing operation.</param>\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposedValue)\n        {\n            return;\n        }\n\n        if (disposing)\n        {\n            ocelotClient?.Dispose();\n            ocelotServer?.Dispose();\n            ocelotHost?.Dispose();\n            response?.Dispose();\n            handler.Dispose();\n            DeleteFiles();\n            DeleteFolders();\n        }\n\n        _disposedValue = true;\n    }\n\n    protected virtual void DeleteFiles()\n    {\n        foreach (var file in Files)\n        {\n            if (!File.Exists(file))\n                continue;\n\n            try\n            {\n                File.Delete(file);\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(e);\n            }\n        }\n        Files.Clear();\n    }\n\n    protected virtual void DeleteFolders()\n    {\n        foreach (var folder in Folders)\n        {\n            try\n            {\n                var f = new DirectoryInfo(folder);\n                if (f.Exists && f.FullName != AppContext.BaseDirectory)\n                {\n                    f.Delete(true);\n                }\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(e);\n            }\n        }\n        Folders.Clear();\n    }\n    #endregion\n}\n"
  },
  {
    "path": "testing/Authentication/AuthenticationSteps.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.IdentityModel.Tokens;\nusing Ocelot.Authorization;\nusing Ocelot.Configuration.File;\nusing Ocelot.DependencyInjection;\nusing Shouldly;\nusing System.Data;\nusing System.IdentityModel.Tokens.Jwt;\nusing System.Net.Http.Json;\nusing System.Runtime.CompilerServices;\nusing System.Security.Claims;\nusing System.Text;\nusing System.Text.Json;\n\nnamespace Ocelot.Testing.Authentication;\n\npublic class AuthenticationSteps : AcceptanceSteps\n{\n    protected BearerToken? token;\n    private readonly Dictionary<string, WebApplication> _jwtSigningServers;\n    public string JwtSigningServerUrl { get => _jwtSigningServers.First().Key; }\n\n    public AuthenticationSteps() : base()\n    {\n        _jwtSigningServers = [];\n    }\n\n    public override void Dispose()\n    {\n        foreach (var kv in _jwtSigningServers)\n        {\n            IDisposable server = _jwtSigningServers[kv.Key];\n            server?.Dispose();\n        }\n        _jwtSigningServers.Clear();\n        base.Dispose();\n        GC.SuppressFinalize(this);\n    }\n\n    protected void WithThreemammalsOptions(JwtBearerOptions o)\n    {\n        o.Audience = AuthToken.Audience; // \"threemammals.com\";\n        o.Authority = new Uri(JwtSigningServerUrl).Authority;\n        o.RequireHttpsMetadata = false;\n        o.TokenValidationParameters = new()\n        {\n            ValidateIssuer = true,\n            ValidIssuer = new Uri(JwtSigningServerUrl).Authority,\n            ValidateAudience = true,\n            ValidAudience = ocelotClient?.BaseAddress?.Authority,\n            ValidateIssuerSigningKey = true,\n            IssuerSigningKey = AuthToken.IssuerSigningKey(),\n        };\n    }\n\n    public void WithJwtBearerAuthentication(IServiceCollection services)\n        => WithJwtBearerAuthentication(services, true);\n    public void WithJwtBearerAuthentication(IServiceCollection services, bool addOcelot)\n    {\n        if (addOcelot) services.AddOcelot();\n        services.AddAuthentication().AddJwtBearer(WithThreemammalsOptions);\n    }\n\n    public static /*IHost*/ WebApplication CreateJwtSigningServer(string url, string[] apiScopes)\n    {\n        apiScopes ??= [OcelotScopes.Api];\n        var builder = TestWebBuilder.CreateSlimBuilder();\n        builder.WebHost.UseUrls(url);\n        builder.Services\n            .AddLogging()\n            .AddAuthentication(options =>\n            {\n                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;\n                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;\n            })\n            .AddJwtBearer(options =>\n            {\n                options.TokenValidationParameters = new TokenValidationParameters\n                {\n                    ValidateIssuer = true,\n                    ValidateAudience = true,\n                    ValidateLifetime = true,\n                    ValidateIssuerSigningKey = true,\n                    ValidIssuer = \"threemammals.com\", // see mycert2.pfx\n                    ValidAudience = \"threemammals.com\",\n                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(\"Ocelot.AcceptanceTests.Authentication\")),\n                };\n            });\n        var app = builder.Build();\n        app.MapGet(\"/connect\", () => \"Hello! Connected!\");\n        app.MapPost(\"/token\", (AuthenticationTokenRequest model) =>\n        {\n            // The signing server should be eligible to sign predefined claims as specified in its configuration.\n            // If an unknown scope or claim is requested for inclusion in a JWT, the server should reject the request.\n            // Therefore, the server configuration should be well-known to the client; otherwise, it poses a security risk.\n            if (!apiScopes.Intersect(model.Scopes?.Split(' ') ?? []).Any())\n            {\n                return Results.BadRequest();\n            }\n            var token = GenerateToken(url, model);\n            return Results.Json(token);\n        });\n        return app;\n    }\n\n    protected static async Task VerifyJwtSigningServerStarted(string url, CancellationToken token, HttpClient? client = null)\n    {\n        client ??= new HttpClient();\n        var response = await client.GetAsync($\"{url}/connect\", token);\n        response.EnsureSuccessStatusCode();\n        var json = await response.Content.ReadAsStringAsync(token);\n        json.ShouldNotBeNullOrEmpty();\n    }\n\n    public Task<string> GivenThereIsExternalJwtSigningService(string[] extraScopes, CancellationToken token)\n    {\n        List<string> scopes = [OcelotScopes.Api, OcelotScopes.Api2];\n        scopes.AddRange(extraScopes);\n        var url = DownstreamUrl(PortFinder.GetRandomPort());\n        var server = CreateJwtSigningServer(url, [.. scopes]);\n        _jwtSigningServers.Add(url, server);\n        return server.StartAsync(token)\n            .ContinueWith(t => VerifyJwtSigningServerStarted(url, token), token)\n            .ContinueWith(t => url, token);\n    }\n\n    public void GivenIHaveAddedATokenToMyRequest() => GivenIHaveAddedATokenToMyRequest(token);\n    public void GivenIHaveAddedATokenToMyRequest(BearerToken? token)\n        => GivenIHaveAddedATokenToMyRequest(token?.AccessToken ?? string.Empty, JwtBearerDefaults.AuthenticationScheme);\n\n    public AuthenticationTokenRequest GivenAuthTokenRequest(string scope,\n        IEnumerable<KeyValuePair<string, string>>? claims = null,\n        [CallerMemberName] string testName = \"\")\n    {\n        var auth = new AuthenticationTokenRequest()\n        {\n            Audience = ocelotClient?.BaseAddress?.Authority ?? string.Empty, // Ocelot DNS is token audience\n            ApiSecret = testName, // \"secret\",\n            Scopes = scope ?? OcelotScopes.Api,\n            Claims = claims is null ? new() : new(claims),\n            UserId = testName,\n            UserName = testName,\n        };\n        return auth;\n    }\n\n    public Task<BearerToken?> GivenIHaveAToken([CallerMemberName] string testName = \"\")\n        => GivenIHaveAToken(OcelotScopes.Api, null, JwtSigningServerUrl, null, testName);\n\n    public async Task<BearerToken?> GivenIHaveAToken(string scope,\n        IEnumerable<KeyValuePair<string, string>>? claims = null,\n        string? issuerUrl = null,\n        string? audience = null,\n        [CallerMemberName] string testName = \"\")\n    {\n        var auth = GivenAuthTokenRequest(scope, claims, testName);\n        auth.Audience = audience ?? ocelotClient?.BaseAddress?.Authority ?? string.Empty;\n        return token = await GivenToken(auth, string.Empty, issuerUrl);\n    }\n    public async Task<BearerToken?> GivenIHaveATokenWithUrlPath(string path, string scope, [CallerMemberName] string testName = \"\")\n    {\n        var auth = GivenAuthTokenRequest(scope, null!, testName);\n        return token = await GivenToken(auth, path);\n    }\n\n    public readonly Dictionary<string, AuthenticationTokenRequest> AuthTokens = [];\n    public AuthenticationTokenRequest AuthToken => AuthTokens.Count > 0 ? AuthTokens.First().Value : new();\n    public event EventHandler<AuthenticationTokenRequestEventArgs>? AuthTokenRequesting;\n    protected virtual void OnAuthenticationTokenRequest(AuthenticationTokenRequestEventArgs e)\n        => AuthTokenRequesting?.Invoke(this, e);\n\n    protected async Task<BearerToken?> GivenToken(AuthenticationTokenRequest auth, string path = \"\", string? issuerUrl = null)\n    {\n        using var http = new HttpClient();\n        issuerUrl ??= JwtSigningServerUrl;\n\n        AuthTokens[issuerUrl] = auth;\n        OnAuthenticationTokenRequest(new(auth));\n\n        var tokenUrl = $\"{issuerUrl + path}/token\";\n        var content = JsonContent.Create(auth);\n        var response = await http.PostAsync(tokenUrl, content);\n        var responseContent = await response.Content.ReadAsStringAsync();\n        response.EnsureSuccessStatusCode();\n        return JsonSerializer.Deserialize<BearerToken>(responseContent, JsonSerializerOptions.Web);\n    }\n\n    protected FileRoute GivenAuthRoute(int port, string path, FileAuthenticationOptions options)\n    {\n        FileRoute? r = GivenRoute(port, path, path) as FileRoute;\n        r!.AuthenticationOptions = options;\n        return r;\n    }\n\n    public FileRoute GivenAuthRoute(int port,\n        string scheme = JwtBearerDefaults.AuthenticationScheme,\n        bool allowAnonymous = false,\n        string[]? scopes = null,\n        string? method = null)\n    {\n        // FileRoute r = GivenDefaultRoute(port)?.WithMethods(method ?? HttpMethods.Get) as FileRoute;\n        FileRoute r = GivenDefaultRoute(port);\n        r.UpstreamHttpMethod.Add(method ?? HttpMethods.Get);\n        r.AuthenticationOptions = new(scheme)\n        {\n            AllowAnonymous = allowAnonymous,\n            AllowedScopes = scopes?.ToList(),\n        };\n        return r;\n    }\n\n    public static FileGlobalConfiguration GivenGlobalAuthConfiguration(\n        string scheme = JwtBearerDefaults.AuthenticationScheme,\n        string[]? allowedScopes = null)\n        => new()\n        {\n            AuthenticationOptions = new()\n            {\n                AllowedScopes = [.. allowedScopes ?? []],\n                AuthenticationProviderKeys = [scheme],\n            },\n        };\n\n    //private IConfiguration _config;\n    private readonly UserManager<IdentityUser>? _userManager = default;\n    public async Task<BearerToken> GenerateTokenAsync(IdentityUser user, string issuer, string audience, string secretKey)\n    {\n        var userClaims = await _userManager?.GetClaimsAsync(user)!;\n        var roles = await _userManager.GetRolesAsync(user);\n        var roleClaims = roles\n            .Select(role => new Claim(ClaimTypes.Role, role));\n        var claims = new List<Claim>\n        {\n            new(JwtRegisteredClaimNames.Sub, user.Id),\n            new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),\n            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),\n        }\n        .Union(userClaims)\n        .Union(roleClaims);\n\n        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));\n        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);\n        var expiry = DateTime.UtcNow.AddMinutes(1);\n        var token = new JwtSecurityToken(\n            issuer: issuer, //_config[\"Jwt:Issuer\"],\n            audience: audience, // _config[\"Jwt:Audience\"],\n            claims: claims,\n            expires: expiry,\n            signingCredentials: creds\n        );\n        var jwt = new JwtSecurityTokenHandler().WriteToken(token);\n        BearerToken bt = new()\n        {\n            AccessToken = jwt,\n            ExpiresIn = (int)(expiry - DateTime.UtcNow).TotalSeconds,\n            TokenType = JwtBearerDefaults.AuthenticationScheme,\n        };\n        return bt;\n    }\n\n    private static bool IsRoleKey(KeyValuePair<string, string> kv)\n        => nameof(ClaimTypes.Role).Equals(kv.Key, StringComparison.OrdinalIgnoreCase)\n            || ClaimTypes.Role.Equals(kv.Key);\n    private static bool IsNotRoleKey(KeyValuePair<string, string> kv)\n        => !IsRoleKey(kv);\n\n    public static BearerToken GenerateToken(string issuerUrl, AuthenticationTokenRequest auth)\n    {\n        if (auth is null)\n            return new();\n\n        var userClaims = auth.Claims // await _userManager.GetClaimsAsync(user);\n            .Where(IsNotRoleKey)\n            .Select(kv => new Claim(kv.Key, kv.Value))\n            .ToList();\n        var roleClaims = auth.Claims // await _userManager.GetRolesAsync(user);\n            .Where(IsRoleKey)\n            .Select(kv => new Claim(/*ClaimTypes.Role*/kv.Key, kv.Value)) // ClaimTypes.Role is not supported, see AuthorizationTests.Should_fix_issue_240\n            .ToList();\n        var claims = new List<Claim>(4 + auth.Claims.Count)\n        {\n            new(JwtRegisteredClaimNames.Sub, auth.UserId ?? string.Empty),\n            new(OcelotClaims.OcSub, auth.UserId ?? string.Empty), // this is a handy lifehack to fix current authorization services like IScopesAuthorizer and IClaimsAuthorizer, which don't support JWT standard and claim types in URL form, aka the ':' delimiter issue with the JSON configuration provider\n            new(JwtRegisteredClaimNames.Email, $\"{auth.UserName}@ocelot.net\"),\n            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),\n            new(ScopesAuthorizer.Scope, auth.Scopes ?? string.Empty),\n        };\n        claims.AddRange(roleClaims);\n        claims.AddRange(userClaims);\n\n        var credentials = new SigningCredentials(auth.IssuerSigningKey(), SecurityAlgorithms.HmacSha256);\n        var expiry = DateTime.UtcNow.AddMinutes(1);\n        var token = new JwtSecurityToken(\n            issuer: new Uri(issuerUrl).Authority, // URL http://localhost:1234 -> DNS localhost:1234 //_config[\"Jwt:Issuer\"],\n            audience: auth.Audience, // _config[\"Jwt:Audience\"],\n            claims: claims,\n            expires: expiry,\n            signingCredentials: credentials\n        );\n        var jwt = string.Empty;\n        try\n        {\n            jwt = new JwtSecurityTokenHandler().WriteToken(token);\n        }\n        catch (Exception ex)\n        {\n            jwt = ex.Message;\n        }\n        BearerToken bt = new()\n        {\n            AccessToken = jwt,\n            ExpiresIn = (int)(expiry - DateTime.UtcNow).TotalSeconds,\n            TokenType = JwtBearerDefaults.AuthenticationScheme,\n        };\n        return bt;\n    }\n\n    public static FileAuthenticationOptions GivenOptions(bool? allowAnonymous = null,\n        List<string>? allowedScopes = null, string[]? schemes = null)\n        => new()\n        {\n            AllowAnonymous = allowAnonymous,\n            AllowedScopes = allowedScopes,\n            AuthenticationProviderKeys = schemes,\n        };\n}\n"
  },
  {
    "path": "testing/Authentication/AuthenticationTokenRequest.cs",
    "content": "﻿using Microsoft.IdentityModel.Tokens;\nusing Ocelot.Infrastructure.Extensions;\nusing System.Text;\nusing System.Text.Json.Serialization;\n\nnamespace Ocelot.Testing.Authentication;\n\npublic class AuthenticationTokenRequest\n{\n    [JsonInclude]\n    public string? Audience { get; set; }\n\n    [JsonInclude]\n    public string? UserId { get; set; }\n\n    [JsonInclude]\n    public string? UserName { get; set; }\n\n    [JsonInclude]\n    public string ApiSecret\n    {\n        get => _apiSecret;\n        set\n        {\n            _apiSecret = value;\n            _issuerSigningKey = null;\n        }\n    }\n\n    [JsonInclude]\n    public string? Scopes { get; set; }\n\n    [JsonInclude]\n    public List<KeyValuePair<string, string>> Claims { get; set; } = [];\n\n    private SymmetricSecurityKey? _issuerSigningKey;\n    private string _apiSecret = string.Empty;\n\n    public SymmetricSecurityKey? IssuerSigningKey()\n    {\n        if (_issuerSigningKey != null)\n            return _issuerSigningKey;\n        if (_apiSecret.IsEmpty())\n            return _issuerSigningKey = null;\n\n        // System.ArgumentOutOfRangeException: 'IDX10720: Unable to create KeyedHashAlgorithm for algorithm 'HS256', the key size must be greater than: '256' bits, key has '160' bits. (Parameter 'keyBytes')'\n        // Make sure the security key is at least 32 characters long,\n        // So, multiply the password body by repeating it.\n        int size = 256 / 8,\n            length = _apiSecret.Length;\n        var securityKey = length >= size ? _apiSecret\n            : string.Join('|', Enumerable.Repeat(_apiSecret, size / length))\n                + _apiSecret[..(size % length)]; // total length should be 32 chars\n        return _issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));\n    }\n}\n"
  },
  {
    "path": "testing/Authentication/AuthenticationTokenRequestEventArgs.cs",
    "content": "﻿namespace Ocelot.Testing.Authentication;\n\npublic class AuthenticationTokenRequestEventArgs(AuthenticationTokenRequest request) : EventArgs\n{\n    public AuthenticationTokenRequest Request { get; } = request;\n}\n"
  },
  {
    "path": "testing/Authentication/OcelotScopes.cs",
    "content": "﻿namespace Ocelot.Testing.Authentication;\n\npublic static class OcelotScopes\n{\n    public const string Api = \"api\";\n    public const string Api2 = \"api2\";\n    public const string OcAdmin = \"oc-admin\";\n}\n\npublic static class OcelotClaims\n{\n    public const string OcSub = \"oc-sub\";\n}\n"
  },
  {
    "path": "testing/BearerToken.cs",
    "content": "using Newtonsoft.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Ocelot.Testing;\n\npublic class BearerToken\n{\n    [JsonInclude]\n    [JsonProperty(\"access_token\")]\n    public string? AccessToken { get; set; }\n\n    [JsonInclude]\n    [JsonProperty(\"expires_in\")]\n    public int ExpiresIn { get; set; }\n\n    [JsonInclude]\n    [JsonProperty(\"token_type\")]\n    public string? TokenType { get; set; }\n}\n"
  },
  {
    "path": "testing/Boxing/Box.cs",
    "content": "﻿namespace Ocelot.Testing.Boxing;\n\npublic class Box\n{\n    public static TResult In<TResult, TBoxee>(TBoxee instance)\n        where TBoxee : class\n        where TResult : Box<TBoxee>\n        => (TResult)Activator.CreateInstance(typeof(TResult), instance)!;\n    public static TResult With<TResult, TBoxee>(TBoxee instance)\n        where TBoxee : class\n        where TResult : Box<TBoxee>\n        => In<TResult, TBoxee>(instance);\n}\n\npublic class Box<T> : Box\n{\n    protected readonly T Instance;\n    protected readonly Type Me;\n    public Box(T instance, string? typeName = null)\n    {\n        if (instance?.GetType().FullName != typeName)\n            throw new ArgumentException($\"Is not type of {typeName ?? \"?\"}\", nameof(instance));\n        Instance = instance;\n        Me = typeof(T);\n    }\n\n    public T Out() => Instance;\n    public T Unbox() => Instance;\n}\n"
  },
  {
    "path": "testing/Boxing/FileRouteBox.cs",
    "content": "﻿using Ocelot.Configuration.File;\nusing System.Collections;\n\nnamespace Ocelot.Testing.Boxing;\n\npublic static class FileRouteBox\n{\n    public static FileRouteBox<T> In<T>(T route) where T : FileRoute\n        => new(route);\n    public static FileRouteBox<T> With<T>(T route) where T : FileRoute\n        => new(route);\n}\n\npublic class FileRouteBox<T>(T route) : Box<T>(route, \"Ocelot.Configuration.File.FileRoute\")\n    where T : FileRoute\n{\n    public static FileRouteBox<T> In(T route) => new(route);\n    public static FileRouteBox<T> With(T route) => new(route);\n\n    public FileRouteBox<T> Hosts<H>(params H[] hosts) where H : class // FileHostAndPort\n    {\n        //route.DownstreamHostAndPorts.AddRange(hosts);\n        var property = Me.GetProperty(\"DownstreamHostAndPorts\");\n        IList? downstreamHostAndPorts = property?.GetValue(Instance) as IList;\n        ArgumentNullException.ThrowIfNull(downstreamHostAndPorts);\n        for (int i = 0; i < hosts.Length; i++)\n        {\n            var host = hosts[i];\n            if (host?.GetType().FullName != \"Ocelot.Configuration.File.FileHostAndPort\")\n                throw new ArgumentException($\"Argument at index {i} is not type of Ocelot.Configuration.File.FileHostAndPort\", nameof(hosts));\n            downstreamHostAndPorts.Add(host);\n        }\n        return this;\n    }\n\n    public FileRouteBox<T> Priority(int priority)\n    {\n        //route.Priority = priority;\n        var property = Me.GetProperty(\"Priority\");\n        property?.SetValue(Instance, priority);\n        return this;\n    }\n\n    public FileRouteBox<T> Methods(params string[] methods)\n    {\n        //route.UpstreamHttpMethod = [.. methods];\n        var property = Me.GetProperty(\"UpstreamHttpMethod\") ?? throw new NullReferenceException(\"UpstreamHttpMethod\");\n        var collection = Activator.CreateInstance(property.PropertyType, (IEnumerable<string>)methods);\n        property.SetValue(Instance, collection);\n        return this;\n    }\n\n    public FileRouteBox<T> UpstreamHeaderTransform(params KeyValuePair<string, string>[] pairs)\n    {\n        //route.UpstreamHeaderTransform = new Dictionary<string, string>(pairs);\n        var property = Me.GetProperty(\"UpstreamHeaderTransform\");\n        IDictionary<string, string> upstreamHeaderTransform = property?.GetValue(Instance) as IDictionary<string, string>\n            ?? throw new ArgumentNullException(nameof(upstreamHeaderTransform));\n        for (int i = 0; i < pairs.Length; i++)\n        {\n            var kv = pairs[i];\n            upstreamHeaderTransform.Add(kv.Key, kv.Value);\n        }\n        return this;\n    }\n\n    public FileRouteBox<T> UpstreamHeaderTransform(string key, string value)\n    {\n        //route.UpstreamHeaderTransform.TryAdd(key, Instance);\n        var property = Me.GetProperty(\"UpstreamHeaderTransform\");\n        IDictionary<string, string> upstreamHeaderTransform = property?.GetValue(Instance) as IDictionary<string, string>\n            ?? throw new ArgumentNullException(nameof(upstreamHeaderTransform));\n        upstreamHeaderTransform.TryAdd(key, value);\n        return this;\n    }\n\n    public FileRouteBox<T> DownstreamHeaderTransform(string key, string value)\n    {\n        //route.DownstreamHeaderTransform.TryAdd(key, value);\n        var property = Me.GetProperty(\"DownstreamHeaderTransform\");\n        IDictionary<string, string> downstreamHeaderTransform = property?.GetValue(Instance) as IDictionary<string, string>\n            ?? throw new ArgumentNullException(nameof(downstreamHeaderTransform));\n        downstreamHeaderTransform.TryAdd(key, value);\n        return this;\n    }\n\n    public FileRouteBox<T> HandlerOptions<O>(O options) where O : class // FileHttpHandlerOptions\n    {\n        //route.HttpHandlerOptions = options;\n        if (options?.GetType().FullName != \"Ocelot.Configuration.File.FileHttpHandlerOptions\")\n            throw new ArgumentException($\"Is not type of Ocelot.Configuration.File.FileHttpHandlerOptions\", nameof(options));\n        var property = Me.GetProperty(\"HttpHandlerOptions\");\n        property?.SetValue(Instance, options);\n        return this;\n    }\n\n    public FileRouteBox<T> Key(string? key)\n    {\n        //route.Key = key;\n        var property = Me.GetProperty(\"Key\");\n        property?.SetValue(Instance, key);\n        return this;\n    }\n\n    public FileRouteBox<T> UpstreamHost(string? upstreamHost)\n    {\n        //route.UpstreamHost = upstreamHost;\n        var property = Me.GetProperty(\"UpstreamHost\");\n        property?.SetValue(Instance, upstreamHost);\n        return this;\n    }\n}\n"
  },
  {
    "path": "testing/Boxing/FileRouteExtensions.cs",
    "content": "﻿using Ocelot.Configuration.File;\n\nnamespace Ocelot.Testing.Boxing;\n\n/// <summary>\n/// For type: <see cref=\"FileRoute\"/>\n/// </summary>\npublic static class FileRouteExtensions\n{\n    public static R WithHosts<R, H>(this R route, params H[] hosts)\n        where R : FileRoute\n        where H : FileHostAndPort\n        => Box.In<FileRouteBox<R>, R>(route).Hosts(hosts).Out(); //route.DownstreamHostAndPorts.AddRange(hosts);\n\n    public static R WithPriority<R>(this R route, int priority)\n        where R : FileRoute\n        => FileRouteBox<R>.In(route).Priority(priority).Out(); //route.Priority = priority;\n\n    public static R WithMethods<R>(this R route, params string[] methods)\n        where R : FileRoute\n        => FileRouteBox.In(route).Methods(methods).Out(); //route.UpstreamHttpMethod = [.. methods];\n\n    public static R WithUpstreamHeaderTransform<R>(this R route, params KeyValuePair<string, string>[] pairs)\n        where R : FileRoute\n        => new FileRouteBox<R>(route).UpstreamHeaderTransform(pairs).Out(); //route.UpstreamHeaderTransform = new Dictionary<string, string>(pairs);\n\n    public static R WithUpstreamHeaderTransform<R>(this R route, string key, string value)\n        where R : FileRoute\n        => new FileRouteBox<R>(route).UpstreamHeaderTransform(key, value).Out(); //route.UpstreamHeaderTransform.TryAdd(key, value);\n\n    public static R WithDownstreamHeaderTransform<R>(this R route, string key, string value)\n        where R : FileRoute\n        => new FileRouteBox<R>(route).DownstreamHeaderTransform(key, value).Out(); //route.DownstreamHeaderTransform.TryAdd(key, value);\n\n    public static R WithHttpHandlerOptions<R, O>(this R route, O options)\n        where R : FileRoute\n        where O : FileHttpHandlerOptions\n        => new FileRouteBox<R>(route).HandlerOptions(options).Out(); //route.HttpHandlerOptions = options;\n\n    public static R WithKey<R>(this R route, string? key)\n        where R : FileRoute\n        => new FileRouteBox<R>(route).Key(key).Out(); //route.Key = key;\n\n    public static R WithUpstreamHost<R>(this R route, string? upstreamHost)\n        where R : FileRoute\n        => new FileRouteBox<R>(route).UpstreamHost(upstreamHost).Out(); //route.UpstreamHost = upstreamHost;\n}\n"
  },
  {
    "path": "testing/FileUnit.cs",
    "content": "﻿namespace Ocelot.Testing;\n\npublic class FileUnit : Unit, IDisposable\n{\n    protected string primaryConfigFileName;\n    protected string globalConfigFileName;\n    protected string environmentConfigFileName;\n    protected readonly List<string> files;\n    protected readonly List<string> folders;\n\n    protected FileUnit() : this(null) { }\n\n    protected FileUnit(string? folder)\n    {\n        folder ??= TestID;\n        Directory.CreateDirectory(folder);\n        folders = [folder];\n\n        primaryConfigFileName = Path.Combine(folder, /*ConfigurationBuilderExtensions.PrimaryConfigFile*/ \"ocelot.json\");\n        globalConfigFileName = Path.Combine(folder, /*ConfigurationBuilderExtensions.GlobalConfigFile*/ \"ocelot.global.json\");\n        environmentConfigFileName = Path.Combine(folder, string.Format(/*ConfigurationBuilderExtensions.EnvironmentConfigFile*/\"ocelot.{0}.json\", EnvironmentName()));\n        files =\n        [\n            primaryConfigFileName,\n            globalConfigFileName,\n            environmentConfigFileName,\n        ];\n    }\n\n    protected virtual string EnvironmentName() => TestID;\n\n    public virtual void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    private bool _disposed;\n\n    /// <summary>\n    /// Protected implementation of Dispose pattern.\n    /// </summary>\n    /// <param name=\"disposing\">Flag to trigger actual disposing operation.</param>\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n        {\n            return;\n        }\n\n        if (disposing)\n        {\n            DeleteFiles();\n            DeleteFolders();\n        }\n\n        _disposed = true;\n    }\n\n    protected void DeleteFiles()\n    {\n        foreach (var file in files)\n        {\n            try\n            {\n                var f = new FileInfo(file);\n                if (f.Exists)\n                {\n                    f.Delete();\n                }\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(e);\n            }\n        }\n    }\n\n    protected void DeleteFolders()\n    {\n        foreach (var folder in folders)\n        {\n            try\n            {\n                var f = new DirectoryInfo(folder);\n                if (f.Exists && f.FullName != AppContext.BaseDirectory)\n                {\n                    f.Delete(true);\n                }\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "testing/Ocelot.Testing.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>\n    <IncludeSymbols>True</IncludeSymbols>\n    <SymbolPackageFormat>snupkg</SymbolPackageFormat>\n    <!--Package properties-->\n    <Version>25.0.0-beta.4</Version>\n    <!-- <VersionPrefix>0.0.0-dev</VersionPrefix> -->\n    <PackageId>Ocelot.Testing</PackageId>\n    <PackageDescription>A shared library for testing the Ocelot core library and its extension packages, including both acceptance and unit tests</PackageDescription>\n    <PackageReleaseNotes>https://github.com/ThreeMammals/Ocelot/blob/main/ReleaseNotes.md</PackageReleaseNotes>\n    <PackageIcon>ocelot_icon.png</PackageIcon>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <PackageLicenseFile>LICENSE.md</PackageLicenseFile>\n    <PackageTags>dotnet;ocelot;testing</PackageTags>\n    <PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/ThreeMammals/Ocelot.git</RepositoryUrl>\n    <Authors>Tom Pallister, Raman Maksimchuk</Authors>\n    <Company>Three Mammals</Company>\n    <Product>Ocelot Gateway</Product>\n    <Copyright>© 2026 Three Mammals. MIT licensed OSS</Copyright>\n    <CodeAnalysisRuleSet>..\\codeanalysis.ruleset</CodeAnalysisRuleSet>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\LICENSE.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\LICENSE.md\" />\n    <None Include=\"README.md\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\README.md\" />\n    <None Include=\"..\\ocelot_icon.png\" Pack=\"true\" PackagePath=\"\\\" Link=\".package\\ocelot_icon.png\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\src\\Ocelot\\Ocelot.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Moq\" Version=\"4.20.72\" />\n    <PackageReference Include=\"Shouldly\" Version=\"4.3.0\" />\n    <PackageReference Include=\"System.Text.Json\" Version=\"10.0.5\" />\n  </ItemGroup>\n\n  <!-- Conditionally obtain references for the .NET 8.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net8.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"8.0.25\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"8.0.25\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"8.0.25\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 9.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net9.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"9.0.14\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"9.0.14\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"9.0.14\" />\n  </ItemGroup>\n  <!-- Conditionally obtain references for the .NET 10.0 target -->\n  <ItemGroup Condition=\" '$(TargetFramework)' == 'net10.0' \">\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.NewtonsoftJson\" Version=\"10.0.5\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"10.0.5\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testing/Ocelot.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing System.Reflection;\n\nnamespace Ocelot.Testing;\n\ninternal class Ocelot\n{\n    private static Assembly? ocelotAssembly;\n    public static Assembly OcelotAssembly { get => ocelotAssembly ??= Assembly.Load(new AssemblyName(\"Ocelot\")); }\n\n    public static object? CreateFileRoute(out Type type)\n    {\n        type = OcelotAssembly.GetType(\"Ocelot.Configuration.File.FileRoute\")!;\n        return Activator.CreateInstance(type);\n    }\n\n    public static object? CreateFileConfiguration(out Type type)\n    {\n        type = OcelotAssembly.GetType(\"Ocelot.Configuration.File.FileConfiguration\")!;\n        return Activator.CreateInstance(type);\n    }\n\n    public static object? CreateFileHostAndPort(string host, int port, out Type type)\n    {\n        type = OcelotAssembly.GetType(\"Ocelot.Configuration.File.FileHostAndPort\")!;\n        return Activator.CreateInstance(type, host, port);\n    }\n\n    public static object? AddOcelot(IServiceCollection services)\n    {\n        var type = OcelotAssembly.GetType(\"Ocelot.DependencyInjection.ServiceCollectionExtensions\")!;\n        var method = type.GetMethods(BindingFlags.Static | BindingFlags.Public)\n            .FirstOrDefault(m =>\n                m.Name == \"AddOcelot\" &&\n                m.GetParameters().Length == 1 &&\n                m.GetParameters()[0].ParameterType == typeof(IServiceCollection));\n        return method?.Invoke(null, [services]);\n    }\n\n    public static Task<IApplicationBuilder> UseOcelot(IApplicationBuilder builder)\n    {\n        var type = OcelotAssembly.GetType(\"Ocelot.Middleware.OcelotMiddlewareExtensions\")!;\n        var method = type.GetMethods(BindingFlags.Static | BindingFlags.Public)\n            .FirstOrDefault(m =>\n                m.Name == \"UseOcelot\" &&\n                m.GetParameters().Length == 1 &&\n                m.GetParameters()[0].ParameterType == typeof(IApplicationBuilder));\n        return method?.Invoke(null, [builder]) as Task<IApplicationBuilder> ?? Task.FromResult(builder);\n    }\n}\n"
  },
  {
    "path": "testing/PortFinder.cs",
    "content": "﻿using System.Collections.Concurrent;\nusing System.Net;\nusing System.Net.Sockets;\n\nnamespace Ocelot.Testing;\n\npublic static class PortFinder\n{\n    private const int EndPortRange = 45000;\n    private static volatile int CurrentPort = 20000;\n    private static readonly object SyncRoot = new();\n\n    //private static readonly ConcurrentBag<int> UsedPorts = new();\n\n    /// <summary>\n    /// Gets a pseudo-random port from the range [<see cref=\"CurrentPort\"/>, <see cref=\"EndPortRange\"/>] for one testing scenario.\n    /// </summary>\n    /// <returns>New allocated port.</returns>\n    /// <exception cref=\"ExceedingPortRangeException\">Critical situation where available ports range has been exceeded.</exception>\n    public static int GetRandomPort()\n    {\n        lock (SyncRoot)\n        {\n            ExceedingPortRangeException.ThrowIf(CurrentPort > EndPortRange);\n            while (!TryUsePort(++CurrentPort));\n            return CurrentPort;\n        }\n    }\n\n    /// <summary>\n    /// Gets the exact number of ports from the range [<see cref=\"CurrentPort\"/>, <see cref=\"EndPortRange\"/>] for one testing scenario.\n    /// </summary>\n    /// <param name=\"count\">The number of wanted ports.</param>\n    /// <returns>Array of allocated ports.</returns>\n    /// <exception cref=\"ExceedingPortRangeException\">Critical situation where available ports range has been exceeded.</exception>\n    public static int[] GetPorts(int count)\n    {\n        var ports = new int[count];\n        for (int i = 0; i < count; i++)\n        {\n            ports[i] = GetRandomPort();\n        }\n        return ports;\n    }\n\n    private static bool TryUsePort(int port)\n    {\n        //UsedPorts.Add(port); // TODO Review or remove, now useless\n        Socket? socket = null;\n        try\n        {\n            var ipe = new IPEndPoint(IPAddress.Loopback, port);\n            socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n            socket.Bind(ipe);\n            socket.Close();\n            return true;\n        }\n        catch\n        {\n            return false;\n        }\n        finally\n        {\n            socket?.Dispose();\n        }\n    }\n}\n\npublic class ExceedingPortRangeException : Exception\n{\n    public ExceedingPortRangeException()\n        : base(\"Cannot find available port to bind to!\") { }\n\n    public static void ThrowIf(bool condition)\n        => _ = condition ? throw new ExceedingPortRangeException() : 0;\n}\n"
  },
  {
    "path": "testing/README.md",
    "content": "# Ocelot.Testing\n[![Release Package](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/release.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/release.yml)\n[![Publish Package](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/publish.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/publish.yml)\n[![NuGet](https://img.shields.io/nuget/v/Ocelot.Testing?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/Ocelot.Testing/ \"Download Ocelot.Testing from NuGet.org\")\n[![Downloads](https://img.shields.io/nuget/dt/Ocelot.Testing?logo=nuget&label=Downloads)](https://www.nuget.org/packages/Ocelot.Testing/ \"Total Ocelot.Testing downloads from NuGet.org\")\n<!-- TODO Create \"Github package downloads\" badge in addition to the NuGet Downloads badge above\nCopilot prompt: Github packages. How to create download badge?\n![GH Pack Downloads](https://github.com/your-username/your-repo/raw/badges/.github/badges/downloads.svg)\n-->\n\nA shared library for testing the [Ocelot](https://github.com/ThreeMammals/Ocelot) core library and its [extension packages](https://www.nuget.org/profiles/ThreeMammals), including both acceptance and unit tests.\n\n- **Core package**: <sub>[![Head package](https://img.shields.io/nuget/v/Ocelot?logo=nuget&label=Ocelot)](https://www.nuget.org/packages/Ocelot/ \"Ocelot package\")</sub>\n- **Core repository**: <sub>[![Head repository](https://img.shields.io/github/v/release/ThreeMammals/Ocelot?logo=github&label=Ocelot)](https://github.com/ThreeMammals/Ocelot/ \"Ocelot repository\")</sub>\n- **Extension packages:** <sub>[![Extension packages](https://img.shields.io/badge/NuGet-ThreeMammals-blue?logo=nuget)](https://www.nuget.org/profiles/ThreeMammals \"Ocelot extension packages owned by ThreeMammals\")</sub>\n"
  },
  {
    "path": "testing/ServiceHandler.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.AspNetCore.TestHost;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing System.Collections.Concurrent;\nusing System.Net;\n\nnamespace Ocelot.Testing;\n\n// TODO 1. Refactor in future to make this class a base class of acceptance steps\n// TODO 2. Develop async versions for each sync method\npublic class ServiceHandler : IDisposable\n{\n#if NET10_0_OR_GREATER\n    private readonly ConcurrentDictionary<string, IHost> _hosts = new();\n#else\n    private readonly ConcurrentDictionary<string, IWebHost> _hosts = new();\n#endif\n\n    public void Dispose()\n    {\n        foreach (var kv in _hosts)\n        {\n            kv.Value?.Dispose();\n        }\n        _hosts.Clear();\n        GC.SuppressFinalize(this);\n    }\n\n    protected Task AddOrStopAsync(\n        string key,\n#if NET10_0_OR_GREATER\n        IHost host)\n#else\n        IWebHost host)\n#endif\n    {\n        if (_hosts.TryAdd(key, host))\n            return Task.CompletedTask;\n\n        var h = _hosts.GetOrAdd(key, host);\n        _hosts[key] = host;\n\n        // Shutdown old host\n        return h.StopAsync().ContinueWith(t => h.Dispose(), TaskContinuationOptions.ExecuteSynchronously);\n    }\n\n    public Task ReleasePortAsync(params int[] ports)\n    {\n        List<Task> tasks = new(ports.Length);\n        foreach (int port in ports)\n        {\n            var kv = _hosts.SingleOrDefault(x => new Uri(x.Key).Port == port);\n            if (kv.Key is null || kv.Value is null)\n                continue;\n\n            var host = kv.Value;\n            var task = host.StopAsync()\n                .ContinueWith(t =>\n                {\n                    host.Dispose();\n                    _hosts.TryRemove(kv);\n                });\n            tasks.Add(task);\n        }\n        return Task.WhenAll(tasks);\n    }\n\n#if NET10_0_OR_GREATER\n    private static IHost CreateHost(Action<IWebHostBuilder> configureWeb)\n#else\n    private static IWebHost CreateHost(Action<IWebHostBuilder> configureWeb)\n#endif\n    {\n#if NET10_0_OR_GREATER\n        var host = TestHostBuilder.CreateHost()\n            .ConfigureWebHost(configureWeb)\n            .Build();\n#else\n        var builder = TestHostBuilder.Create();\n        configureWeb(builder);\n        var host = builder.Build();\n#endif\n        return host;\n    }\n\n#if NET10_0_OR_GREATER\n    public IHost\n#else\n    public IWebHost\n#endif\n        GivenThereIsAServiceRunningOn(string baseUrl, RequestDelegate handler)\n    {\n        void ConfigureWeb(IWebHostBuilder builder) => builder\n            .UseUrls(baseUrl)\n            .UseKestrel()\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .Configure(app => app.Run(handler));\n        var host = CreateHost(ConfigureWeb);\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n        return host;\n    }\n\n    public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, RequestDelegate handler)\n    {\n        void ConfigureWeb(IWebHostBuilder builder) => builder\n            .UseUrls(baseUrl)\n            .UseKestrel()\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .Configure(app => app.UsePathBase(basePath).Run(handler));\n        var host = CreateHost(ConfigureWeb);\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n    }\n\n    public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Action<IServiceCollection> configureServices, RequestDelegate handler)\n    {\n        void ConfigureWeb(IWebHostBuilder builder) => builder\n            .UseUrls(baseUrl)\n            .UseKestrel()\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .ConfigureServices(configureServices)\n            .Configure(app => app.UsePathBase(basePath).Run(handler));\n        var host = CreateHost(ConfigureWeb);\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n    }\n\n    public void GivenThereIsAServiceRunningOnWithKestrelOptions(string baseUrl, string basePath, Action<KestrelServerOptions> options, RequestDelegate handler)\n    {\n        void ConfigureWeb(IWebHostBuilder builder) => builder\n            .UseUrls(baseUrl)\n            .UseKestrel()\n            .ConfigureKestrel(options ?? WithDefaultKestrelServerOptions) // !\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .UseIISIntegration()\n            .Configure(app => app.UsePathBase(basePath).Run(handler));\n        var host = CreateHost(ConfigureWeb);\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n    }\n\n    internal void WithDefaultKestrelServerOptions(KestrelServerOptions options)\n    { }\n\n    public void GivenThereIsAHttpsServiceRunningOn(string baseUrl, string basePath, string fileName, string password, int port, RequestDelegate handler)\n    {\n        void WithKestrelOptions(KestrelServerOptions options)\n            => options.Listen(IPAddress.Loopback, port, o => o.UseHttps(fileName, password));\n        void ConfigureWeb(IWebHostBuilder builder) => builder\n            .UseUrls(baseUrl)\n            .UseKestrel(WithKestrelOptions)\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .Configure(app => app.UsePathBase(basePath).Run(handler));\n        var host = CreateHost(ConfigureWeb);\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n    }\n\n    #region Advanced helpers\n\n    public static string Localhost(int port) => $\"{Uri.UriSchemeHttp}://localhost:{port}\";\n\n    public void GivenThereIsAServiceRunningOn(int port, RequestDelegate handler)\n        => GivenThereIsAServiceRunningOn(Localhost(port), handler);\n\n    public void GivenThereIsAServiceRunningOn(int port, string path, RequestDelegate handler)\n        => GivenThereIsAServiceRunningOn(Localhost(port), path, handler);\n\n    #endregion\n\n#if NET10_0_OR_GREATER\n    public IHost\n#else\n    public IWebHost\n#endif\n    GivenThereIsAServiceRunningOn(int port,\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<WebHostBuilderContext, ILoggingBuilder>? configureLogging,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost)\n    => GivenThereIsAServiceRunningOn(Localhost(port), configureDelegate, configureLogging, configureServices, configureApp, configureWebHost);\n\n#if NET10_0_OR_GREATER\n    public IHost\n#else\n    public IWebHost\n#endif\n    GivenThereIsAServiceRunningOn(string baseUrl,\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<WebHostBuilderContext, ILoggingBuilder>? configureLogging,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost)\n    {\n        void ConfigureWeb(IWebHostBuilder builder)\n        {\n            builder.UseUrls(baseUrl).UseKestrel();\n            if (configureDelegate != null) builder.ConfigureAppConfiguration(configureDelegate);\n            if (configureLogging != null) builder.ConfigureLogging(configureLogging);\n            if (configureServices != null) builder.ConfigureServices(configureServices);\n            if (configureApp != null) builder.Configure(configureApp);\n            configureWebHost?.Invoke(builder);\n        }\n        var host = CreateBuilder(ConfigureWeb).Build();\n        AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult();\n        host.Start();\n        return host;\n    }\n\n#if NET10_0_OR_GREATER\n    public Task<IHost>\n#else\n    public Task<IWebHost>\n#endif\n    GivenThereIsAServiceRunningOnAsync(int port,\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<WebHostBuilderContext, ILoggingBuilder>? configureLogging,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost)\n        => GivenThereIsAServiceRunningOnAsync(Localhost(port), configureDelegate, configureLogging, configureServices, configureApp, configureWebHost);\n\n#if NET10_0_OR_GREATER\n    public Task<IHost>\n#else\n    public Task<IWebHost>\n#endif\n    GivenThereIsAServiceRunningOnAsync(string baseUrl,\n        Action<WebHostBuilderContext, IConfigurationBuilder>? configureDelegate,\n        Action<WebHostBuilderContext, ILoggingBuilder>? configureLogging,\n        Action<IServiceCollection>? configureServices,\n        Action<IApplicationBuilder>? configureApp,\n        Action<IWebHostBuilder>? configureWebHost)\n    {\n        void ConfigureWeb(IWebHostBuilder builder)\n        {\n            builder.UseUrls(baseUrl).UseKestrel();\n            if (configureDelegate != null) builder.ConfigureAppConfiguration(configureDelegate);\n            if (configureLogging != null) builder.ConfigureLogging(configureLogging);\n            if (configureServices != null) builder.ConfigureServices(configureServices);\n            if (configureApp != null) builder.Configure(configureApp);\n            configureWebHost?.Invoke(builder);\n        }\n        var host = CreateBuilder(ConfigureWeb).Build();\n        return AddOrStopAsync(baseUrl, host)\n            .ContinueWith(t => host.StartAsync())\n            .ContinueWith(t => host, TaskContinuationOptions.ExecuteSynchronously);\n    }\n\n#if NET10_0_OR_GREATER\n    private static IHostBuilder\n#else\n    private static IWebHostBuilder\n#endif\n    CreateBuilder(Action<IWebHostBuilder> configureWeb)\n    {\n#if NET10_0_OR_GREATER\n        return TestHostBuilder.CreateHost()\n            .ConfigureWebHost(configureWeb);\n#else\n        var builder = TestHostBuilder.Create();\n        configureWeb(builder);\n        return builder;\n#endif\n    }\n}\n"
  },
  {
    "path": "testing/StreamExtensions.cs",
    "content": "﻿namespace Ocelot.Testing;\n\npublic static class StreamExtensions\n{\n    public static string AsString(this Stream stream)\n    {\n        using var reader = new StreamReader(stream);\n        var text = reader.ReadToEnd();\n        return text;\n    }\n}\n"
  },
  {
    "path": "testing/TestHostBuilder.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Ocelot.Testing;\n\npublic sealed class TestHostBuilder\n#if NET10_0_OR_GREATER\n    : HostBuilder\n#else\n    : WebHostBuilder\n#endif\n{\n#if NET10_0_OR_GREATER\n    public static IHostBuilder Create()\n        => new HostBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes);\n#else\n    public static IWebHostBuilder Create()\n        => new WebHostBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes);\n#endif\n\n#if NET10_0_OR_GREATER\n    public static IHostBuilder Create(Action<ServiceProviderOptions> configure)\n        => new HostBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n#else\n    public static IWebHostBuilder Create(Action<ServiceProviderOptions> configure)\n        => new WebHostBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n#endif\n\n    public static void WithEnabledValidateScopes(ServiceProviderOptions options)\n        => options.ValidateScopes = true;\n\n    public static IHostBuilder CreateHost()\n        => Host.CreateDefaultBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes);\n\n    public static IHostBuilder CreateHost(Action<ServiceProviderOptions> configure)\n        => Host.CreateDefaultBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n}\n"
  },
  {
    "path": "testing/TestWebBuilder.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Ocelot.Testing;\n\npublic static class TestWebBuilder\n{\n    public static void WithEnabledValidateScopes(ServiceProviderOptions options)\n        => options.ValidateScopes = true;\n\n    public static WebApplicationBuilder CreateBuilder()\n    {\n        var builder = WebApplication.CreateBuilder();\n        builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes);\n        return builder;\n    }\n    public static WebApplicationBuilder Create(Action<ServiceProviderOptions> configure)\n    {\n        var builder = WebApplication.CreateBuilder();\n        builder.WebHost.UseDefaultServiceProvider(configure + WithEnabledValidateScopes);\n        return builder;\n    }\n\n    public static WebApplicationBuilder CreateBuilder(string[] args)\n    {\n        var builder = WebApplication.CreateBuilder(args);\n        builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes);\n        return builder;\n    }\n\n    public static WebApplicationBuilder CreateSlimBuilder()\n    {\n        var builder = WebApplication.CreateSlimBuilder();\n        builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes);\n        return builder;\n    }\n}\n"
  },
  {
    "path": "testing/Unit.cs",
    "content": "﻿using Ocelot.Infrastructure.Extensions;\nusing System.Runtime.CompilerServices;\n\nnamespace Ocelot.Testing;\n\n/// <summary>\n/// This is the base class for any unit testing classes.\n/// It is recommended to always inherit from it.\n/// </summary>\npublic class Unit\n{\n    protected readonly Guid _testId = Guid.NewGuid();\n    protected string TestID { get => _testId.ToString(\"N\"); }\n    protected string TestName([CallerMemberName] string? testName = null)\n        => testName.IfEmpty(TestID);\n\n    protected virtual bool IsCiCd() => IsRunningInGitHubActions();\n    protected static bool IsRunningInGitHubActions()\n        => Environment.GetEnvironmentVariable(\"GITHUB_ACTIONS\") == \"true\";\n}\n"
  },
  {
    "path": "testing/Wait.cs",
    "content": "﻿using System.Diagnostics;\n\nnamespace Ocelot.Testing;\n\npublic class Wait\n{\n\n    private readonly int _milliSeconds;\n    public static Wait For(int milliSeconds) => new(milliSeconds);\n\n    private Wait() { }\n    private Wait(int milliSeconds)\n    {\n        _milliSeconds = milliSeconds;\n    }\n\n    public bool Until(Func<bool> condition)\n    {\n        var watcher = Stopwatch.StartNew();\n        while (watcher.ElapsedMilliseconds < _milliSeconds)\n        {\n            if (condition.Invoke())\n            {\n                watcher.Stop();\n                return true;\n            }\n        }\n        watcher.Stop();\n        return false;\n    }\n\n    public async Task<bool> UntilAsync(Func<Task<bool>> condition)\n    {\n        var watcher = Stopwatch.StartNew();\n        while (watcher.ElapsedMilliseconds < _milliSeconds)\n        {\n            if (await condition.Invoke())\n            {\n                watcher.Stop();\n                return true;\n            }\n        }\n        watcher.Stop();\n        return false;\n    }\n}\n"
  }
]