diff --git a/.devcontainer/TestConfig.json b/.devcontainer/TestConfig.json
index 54ff589ce..2ec990b9f 100644
--- a/.devcontainer/TestConfig.json
+++ b/.devcontainer/TestConfig.json
@@ -4,7 +4,6 @@
"SecureServer": "redis",
"FailoverMasterServer": "redis",
"FailoverReplicaServer": "redis",
- "RediSearchServer": "redisearch",
"IPv4Server": "redis",
"RemoteServer": "redis",
"SentinelServer": "redis",
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 2b458776f..a801d6f1e 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -12,16 +12,12 @@ services:
- ./TestConfig.json:/workspace/tests/StackExchange.Redis.Tests/TestConfig.json:ro
depends_on:
- redis
- - redisearch
links:
- "redis:redis"
- - "redisearch:redisearch"
command: /bin/sh -c "while sleep 1000; do :; done"
redis:
build:
context: ../tests/RedisConfigs
dockerfile: Dockerfile
sysctls :
- net.core.somaxconn: '511'
- redisearch:
- image: redislabs/redisearch:latest
\ No newline at end of file
+ net.core.somaxconn: '511'
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 162be69f5..eb05866a0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -27,56 +27,187 @@ indent_size = 2
# Dotnet code style settings:
[*.{cs,vb}]
# Sort using and Import directives with System.* appearing first
-dotnet_sort_system_directives_first = true
+dotnet_sort_system_directives_first = true:warning
# Avoid "this." and "Me." if not necessary
-dotnet_style_qualification_for_field = false:suggestion
-dotnet_style_qualification_for_property = false:suggestion
-dotnet_style_qualification_for_method = false:suggestion
-dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+# Modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_style_readonly_field = true:warning
# Use language keywords instead of framework type names for type references
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
# Suggest more modern language features when available
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_null_propagation = true:warning
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_auto_properties = true:suggestion
# Ignore silly if statements
-dotnet_style_prefer_conditional_expression_over_return = false:none
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+
+# Don't warn on things that actually need suppressing
+dotnet_remove_unnecessary_suppression_exclusions = CA1009,CA1063,CA1069,CA1416,CA1816,CA1822,CA2202,CS0618,IDE0060,IDE0062,RCS1047,RCS1085,RCS1090,RCS1194,RCS1231
+
+# Style Definitions
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# Use PascalCase for constant fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
# CSharp code style settings:
[*.cs]
# Prefer method-like constructs to have a expression-body
-csharp_style_expression_bodied_methods = true:none
-csharp_style_expression_bodied_constructors = true:none
-csharp_style_expression_bodied_operators = true:none
+csharp_style_expression_bodied_constructors = true:silent
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_operators = true:warning
# Prefer property-like constructs to have an expression-body
-csharp_style_expression_bodied_properties = true:none
-csharp_style_expression_bodied_indexers = true:none
-csharp_style_expression_bodied_accessors = true:none
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_lambdas = true:warning
+csharp_style_expression_bodied_local_functions = true:silent
-# Suggest more modern language features when available
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_conditional_delegate_call = true:suggestion
+# Pattern matching preferences
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+
+# Null-checking preferences
+csharp_style_throw_expression = true:warning
+csharp_style_conditional_delegate_call = true:warning
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,volatile,async:suggestion
+
+# Expression-level preferences
+csharp_prefer_braces = true:silent
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_prefer_simple_default_expression = true:silent
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_inlined_variable_declaration = true:warning
+csharp_prefer_simple_using_statement = true:silent
+csharp_style_prefer_not_pattern = true:warning
+csharp_style_prefer_switch_expression = true:warning
-# Newline settings
+# Disable range operator suggestions
+csharp_style_prefer_range_operator = false:none
+csharp_style_prefer_index_operator = false:none
+
+# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true:warning
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+
+# Wrapping preferences
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
+# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852
+# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd
+dotnet_diagnostic.CA1852.severity = warning
+
+# IDE preferences
+dotnet_diagnostic.IDE0090.severity = silent # IDE0090: Use 'new(...)'
+
+#Roslynator preferences
+dotnet_diagnostic.RCS1037.severity = error # RCS1037: Remove trailing white-space.
+dotnet_diagnostic.RCS1098.severity = none # RCS1098: Constant values should be placed on right side of comparisons.
+
+dotnet_diagnostic.RCS1194.severity = none # RCS1194: Implement exception constructors.
+dotnet_diagnostic.RCS1229.severity = none # RCS1229: Use async/await when necessary.
+dotnet_diagnostic.RCS1233.severity = none # RCS1233: Use short-circuiting operator.
+dotnet_diagnostic.RCS1234.severity = none # RCS1234: Duplicate enum value.
+
+# StyleCop preferences
+dotnet_diagnostic.SA0001.severity = none # SA0001: XML comment analysis is disabled
+
+dotnet_diagnostic.SA1101.severity = none # SA1101: Prefix local calls with this
+dotnet_diagnostic.SA1108.severity = none # SA1108: Block statements should not contain embedded comments
+dotnet_diagnostic.SA1122.severity = none # SA1122: Use string.Empty for empty strings
+dotnet_diagnostic.SA1127.severity = none # SA1127: Generic type constraints should be on their own line
+dotnet_diagnostic.SA1128.severity = none # SA1128: Put constructor initializers on their own line
+dotnet_diagnostic.SA1132.severity = none # SA1132: Do not combine fields
+dotnet_diagnostic.SA1133.severity = none # SA1133: Do not combine attributes
+
+dotnet_diagnostic.SA1200.severity = none # SA1200: Using directives should be placed correctly
+dotnet_diagnostic.SA1201.severity = none # SA1201: Elements should appear in the correct order
+dotnet_diagnostic.SA1202.severity = none # SA1202: Elements should be ordered by access
+dotnet_diagnostic.SA1203.severity = none # SA1203: Constants should appear before fields
+
+dotnet_diagnostic.SA1306.severity = none # SA1306: Field names should begin with lower-case letter
+dotnet_diagnostic.SA1309.severity = none # SA1309: Field names should not begin with underscore
+dotnet_diagnostic.SA1310.severity = silent # SA1310: Field names should not contain underscore
+dotnet_diagnostic.SA1311.severity = none # SA1311: Static readonly fields should begin with upper-case letter
+dotnet_diagnostic.SA1312.severity = none # SA1312: Variable names should begin with lower-case letter
+
+dotnet_diagnostic.SA1401.severity = silent # SA1401: Fields should be private
+dotnet_diagnostic.SA1402.severity = suggestion # SA1402: File may only contain a single type
+
+dotnet_diagnostic.SA1503.severity = silent # SA1503: Braces should not be omitted
+dotnet_diagnostic.SA1516.severity = silent # SA1516: Elements should be separated by blank line
+
+dotnet_diagnostic.SA1600.severity = none # SA1600: Elements should be documented
+dotnet_diagnostic.SA1601.severity = none # SA1601: Partial elements should be documented
+dotnet_diagnostic.SA1602.severity = none # SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1615.severity = none # SA1615: Element return value should be documented
+dotnet_diagnostic.SA1623.severity = none # SA1623: Property summary documentation should match accessors
+dotnet_diagnostic.SA1633.severity = none # SA1633: File should have header
+dotnet_diagnostic.SA1642.severity = none # SA1642: Constructor summary documentation should begin with standard text
+dotnet_diagnostic.SA1643.severity = none # SA1643: Destructor summary documentation should begin with standard text
+
+
+# To Fix:
+dotnet_diagnostic.SA1204.severity = none # SA1204: Static elements should appear before instance elements
+dotnet_diagnostic.SA1214.severity = none # SA1214: Readonly fields should appear before non-readonly fields
+dotnet_diagnostic.SA1304.severity = none # SA1304: Non-private readonly fields should begin with upper-case letter
+dotnet_diagnostic.SA1307.severity = none # SA1307: Accessible fields should begin with upper-case letter
+dotnet_diagnostic.SA1308.severity = suggestion # SA1308: Variable names should not be prefixed
+dotnet_diagnostic.SA1131.severity = none # SA1131: Use readable conditions
+dotnet_diagnostic.SA1405.severity = none # SA1405: Debug.Assert should provide message text
+dotnet_diagnostic.SA1501.severity = none # SA1501: Statement should not be on a single line
+dotnet_diagnostic.SA1502.severity = suggestion # SA1502: Element should not be on a single line
+dotnet_diagnostic.SA1513.severity = none # SA1513: Closing brace should be followed by blank line
+dotnet_diagnostic.SA1515.severity = none # SA1515: Single-line comment should be preceded by blank line
+dotnet_diagnostic.SA1611.severity = suggestion # SA1611: Element parameters should be documented
+dotnet_diagnostic.SA1649.severity = suggestion # SA1649: File name should match first type name
+
+
+
-# Space settings
-csharp_space_after_keywords_in_control_flow_statements = true:suggestion
-# Language settings
-csharp_prefer_simple_default_expression = false:none
\ No newline at end of file
diff --git a/.github/.github.csproj b/.github/.github.csproj
index 5a3b2f1f1..008099327 100644
--- a/.github/.github.csproj
+++ b/.github/.github.csproj
@@ -1,5 +1,5 @@
-
+
- netcoreapp3.1
+ net6.0
\ No newline at end of file
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index d3a01e707..0b62bc917 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -1,121 +1,152 @@
-name: CI Builds
+name: CI
on:
pull_request:
push:
- branches:
- - main
+ branches: [ 'main' ]
paths:
- - '*'
- - '!/docs/*' # Don't run workflow when files are only in the /docs directory
+ - '**'
+ - '!/docs/*' # Don't run workflow when files are only in the /docs directory
+ workflow_dispatch:
jobs:
main:
name: StackExchange.Redis (Ubuntu)
runs-on: ubuntu-latest
+ env:
+ DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Enable color output, even though the console output is redirected in Actions
+ TERM: xterm # Enable color output in GitHub Actions
steps:
- - name: Checkout code
- uses: actions/checkout@v1
- - name: Setup .NET Core 3.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '3.1.x'
- - name: Setup .NET 5.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '5.0.x'
- - name: .NET Build
- run: dotnet build Build.csproj -c Release /p:CI=true
- - name: Start Redis Services (docker-compose)
- working-directory: ./tests/RedisConfigs
- run: docker-compose -f docker-compose.yml up -d
- - name: StackExchange.Redis.Tests
- run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true
- - uses: dorny/test-reporter@v1
- continue-on-error: true
- if: success() || failure()
- with:
- name: StackExchange.Redis.Tests (Ubuntu) - Results
- path: 'test-results/*.trx'
- reporter: dotnet-trx
- - name: .NET Lib Pack
- run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true
-
- nredisearch:
- name: NRediSearch (Ubuntu)
- runs-on: ubuntu-latest
- services:
- redisearch:
- image: redislabs/redisearch:latest
- ports:
- - 6385:6379
- steps:
- - name: Checkout code
- uses: actions/checkout@v1
- - name: Setup .NET Core 3.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '3.1.x'
- - name: Setup .NET 5.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '5.0.x'
- - name: .NET Build
- run: dotnet build Build.csproj -c Release /p:CI=true
- - name: NRedisSearch.Tests
- run: dotnet test tests/NRediSearch.Test/NRediSearch.Test.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true
- - uses: dorny/test-reporter@v1
- continue-on-error: true
- if: success() || failure()
- with:
- name: NRedisSearch.Tests - Results
- path: 'test-results/*.trx'
- reporter: dotnet-trx
- - name: .NET Lib Pack
- run: dotnet pack src/NRediSearch/NRediSearch.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch the full history
+ - name: Start Redis Services (docker-compose)
+ working-directory: ./tests/RedisConfigs
+ run: docker compose -f docker-compose.yml up -d --wait
+ - name: Install .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 10.0.x
+ - name: .NET Build
+ run: dotnet build Build.csproj -c Release /p:CI=true
+ - name: StackExchange.Redis.Tests
+ run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true
+ - uses: dorny/test-reporter@v1
+ continue-on-error: true
+ if: success() || failure()
+ with:
+ name: Test Results - Ubuntu
+ path: 'test-results/*.trx'
+ reporter: dotnet-trx
+ - name: .NET Lib Pack
+ run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true
windows:
- name: StackExchange.Redis (Windows Server 2019)
- runs-on: windows-2019
+ name: StackExchange.Redis (Windows Server 2022)
+ runs-on: windows-2022
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline # Disabling signing because of massive perf hit, see https://github.com/NuGet/Home/issues/11548
+ DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Note this doesn't work yet for Windows - see https://github.com/dotnet/runtime/issues/68340
+ TERM: xterm
+ DOCKER_BUILDKIT: 1
steps:
- - name: Checkout code
- uses: actions/checkout@v1
- - name: Setup .NET Core 3.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '3.1.x'
- - name: Setup .NET 5.x
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: '5.0.x'
- - name: .NET Build
- run: dotnet build Build.csproj -c Release /p:CI=true
- - name: Start Redis Services (v3.0.503)
- working-directory: .\tests\RedisConfigs\3.0.503
- run: |
- .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\master-6379.conf"
- .\redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf"
- .\redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf"
- .\redis-server.exe --service-install --service-name "redis-6382" "..\Failover\master-6382.conf"
- .\redis-server.exe --service-install --service-name "redis-6383" "..\Failover\replica-6383.conf"
- .\redis-server.exe --service-install --service-name "redis-7000" "..\Cluster\cluster-7000.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7001" "..\Cluster\cluster-7001.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7002" "..\Cluster\cluster-7002.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7003" "..\Cluster\cluster-7003.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7004" "..\Cluster\cluster-7004.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7005" "..\Cluster\cluster-7005.conf" --dir "..\Cluster"
- .\redis-server.exe --service-install --service-name "redis-7010" "..\Sentinel\redis-7010.conf"
- .\redis-server.exe --service-install --service-name "redis-7011" "..\Sentinel\redis-7011.conf"
- .\redis-server.exe --service-install --service-name "redis-26379" "..\Sentinel\sentinel-26379.conf" --sentinel
- .\redis-server.exe --service-install --service-name "redis-26380" "..\Sentinel\sentinel-26380.conf" --sentinel
- .\redis-server.exe --service-install --service-name "redis-26381" "..\Sentinel\sentinel-26381.conf" --sentinel
- Start-Service redis-*
- - name: StackExchange.Redis.Tests
- run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true
- - uses: dorny/test-reporter@v1
- continue-on-error: true
- if: success() || failure()
- with:
- name: StackExchange.Redis.Tests (Windows Server 2019) - Results
- path: 'test-results/*.trx'
- reporter: dotnet-trx
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch the full history
+ - uses: Vampire/setup-wsl@v2
+ with:
+ distribution: Ubuntu-22.04
+ - name: Install Redis
+ shell: wsl-bash {0}
+ working-directory: ./tests/RedisConfigs
+ run: |
+ apt-get update
+ apt-get install curl gpg lsb-release libgomp1 jq -y
+ curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
+ chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
+ echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
+ apt-get update
+ apt-get install -y redis
+ mkdir redis
+ - name: Run redis-server
+ shell: wsl-bash {0}
+ working-directory: ./tests/RedisConfigs/redis
+ run: |
+ pwd
+ ls .
+ # Run each server instance in order
+ redis-server ../Basic/primary-6379.conf &
+ redis-server ../Basic/replica-6380.conf &
+ redis-server ../Basic/secure-6381.conf &
+ redis-server ../Failover/primary-6382.conf &
+ redis-server ../Failover/replica-6383.conf &
+ redis-server ../Cluster/cluster-7000.conf --dir ../Cluster &
+ redis-server ../Cluster/cluster-7001.conf --dir ../Cluster &
+ redis-server ../Cluster/cluster-7002.conf --dir ../Cluster &
+ redis-server ../Cluster/cluster-7003.conf --dir ../Cluster &
+ redis-server ../Cluster/cluster-7004.conf --dir ../Cluster &
+ redis-server ../Cluster/cluster-7005.conf --dir ../Cluster &
+ redis-server ../Sentinel/redis-7010.conf &
+ redis-server ../Sentinel/redis-7011.conf &
+ redis-server ../Sentinel/sentinel-26379.conf --sentinel &
+ redis-server ../Sentinel/sentinel-26380.conf --sentinel &
+ redis-server ../Sentinel/sentinel-26381.conf --sentinel &
+ # Wait for server instances to get ready
+ sleep 5
+ echo "Checking redis-server version with port 6379"
+ redis-cli -p 6379 INFO SERVER | grep redis_version || echo "Failed to get version for port 6379"
+ echo "Checking redis-server version with port 6380"
+ redis-cli -p 6380 INFO SERVER | grep redis_version || echo "Failed to get version for port 6380"
+ echo "Checking redis-server version with port 6381"
+ redis-cli -p 6381 INFO SERVER | grep redis_version || echo "Failed to get version for port 6381"
+ echo "Checking redis-server version with port 6382"
+ redis-cli -p 6382 INFO SERVER | grep redis_version || echo "Failed to get version for port 6382"
+ echo "Checking redis-server version with port 6383"
+ redis-cli -p 6383 INFO SERVER | grep redis_version || echo "Failed to get version for port 6383"
+ echo "Checking redis-server version with port 7000"
+ redis-cli -p 7000 INFO SERVER | grep redis_version || echo "Failed to get version for port 7000"
+ echo "Checking redis-server version with port 7001"
+ redis-cli -p 7001 INFO SERVER | grep redis_version || echo "Failed to get version for port 7001"
+ echo "Checking redis-server version with port 7002"
+ redis-cli -p 7002 INFO SERVER | grep redis_version || echo "Failed to get version for port 7002"
+ echo "Checking redis-server version with port 7003"
+ redis-cli -p 7003 INFO SERVER | grep redis_version || echo "Failed to get version for port 7003"
+ echo "Checking redis-server version with port 7004"
+ redis-cli -p 7004 INFO SERVER | grep redis_version || echo "Failed to get version for port 7004"
+ echo "Checking redis-server version with port 7005"
+ redis-cli -p 7005 INFO SERVER | grep redis_version || echo "Failed to get version for port 7005"
+ echo "Checking redis-server version with port 7010"
+ redis-cli -p 7010 INFO SERVER | grep redis_version || echo "Failed to get version for port 7010"
+ echo "Checking redis-server version with port 7011"
+ redis-cli -p 7011 INFO SERVER | grep redis_version || echo "Failed to get version for port 7011"
+ echo "Checking redis-server version with port 26379"
+ redis-cli -p 26379 INFO SERVER | grep redis_version || echo "Failed to get version for port 26379"
+ echo "Checking redis-server version with port 26380"
+ redis-cli -p 26380 INFO SERVER | grep redis_version || echo "Failed to get version for port 26380"
+ echo "Checking redis-server version with port 26381"
+ redis-cli -p 26381 INFO SERVER | grep redis_version || echo "Failed to get version for port 26381"
+ continue-on-error: true
+
+ - name: .NET Build
+ run: dotnet build Build.csproj -c Release /p:CI=true
+ - name: StackExchange.Redis.Tests
+ run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true
+ - uses: dorny/test-reporter@v1
+ continue-on-error: true
+ if: success() || failure()
+ with:
+ name: Tests Results - Windows Server 2022
+ path: 'test-results/*.trx'
+ reporter: dotnet-trx
+ # Package and upload to MyGet only on pushes to main, not on PRs
+ - name: .NET Pack
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
+ run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true
+ - name: Upload to MyGet
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
+ run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..a03767211
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,63 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ 'main' ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ 'main' ]
+ workflow_dispatch:
+
+ schedule:
+ - cron: '8 9 * * 1'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'csharp' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+ # Use only 'java' to analyze code written in Java, Kotlin or both
+ # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 10.0.x
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ - if: matrix.language == 'csharp'
+ name: .NET Build
+ run: dotnet build Build.csproj -c Release /p:CI=true
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.gitignore b/.gitignore
index 3d1821b36..c0024fb1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,5 @@ t8.shakespeare.txt
launchSettings.json
*.vsp
*.diagsession
-TestResults/
\ No newline at end of file
+TestResults/
+BenchmarkDotNet.Artifacts/
diff --git a/Build.csproj b/Build.csproj
index 3e16e801c..41fb15b0c 100644
--- a/Build.csproj
+++ b/Build.csproj
@@ -1,5 +1,6 @@
+
diff --git a/Directory.Build.props b/Directory.Build.props
index d43bc25dd..273acae25 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,16 +6,16 @@
$(MSBuildThisFileDirectory)StackExchange.Redis.snk$(AssemblyName)strict
- Stack Exchange, Inc.; marc.gravell
+ Stack Exchange, Inc.; Marc Gravell; Nick Cravertrue$(MSBuildThisFileDirectory)Shared.rulesetNETSDK1069
- NU5105
+ $(NoWarn);NU5105;NU1507;SER001;SER002;SER003;SER004;SER005https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes
- https://github.com/StackExchange/StackExchange.Redis/
+ https://stackexchange.github.io/StackExchange.Redis/MIT
- 8.0
+ 14githttps://github.com/StackExchange/StackExchange.Redis/
@@ -25,16 +25,27 @@
falsetruefalse
+ true
+ 00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912ffftruetruetrue
+
+
-
+
+
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
index ac2529fe4..687e19684 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,30 +1,5 @@
-
-
- $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..9767a0ab1
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index e32afb858..db4620c99 100644
--- a/LICENSE
+++ b/LICENSE
@@ -24,11 +24,11 @@ SOFTWARE.
Third Party Licenses:
-The Redis project (http://redis.io/) is independent of this client library, and
+The Redis project (https://redis.io/) is independent of this client library, and
is licensed separately under the three clause BSD license. The full license
-information can be viewed here: http://redis.io/topics/license
+information can be viewed here: https://redis.io/topics/license
-This tool makes use of the "redis-doc" library from http://redis.io/documentation
+This tool makes use of the "redis-doc" library from https://redis.io/documentation
in the intellisense comments, which is licensed under the
Creative Commons Attribution-ShareAlike 4.0 International license; full
details are available here:
@@ -43,5 +43,5 @@ This tool is not used in the release binaries.
The development solution uses the BookSleeve package from nuget
(https://code.google.com/p/booksleeve/) by Marc Gravell. This is licensed
under the Apache 2.0 license; full details are available here:
-http://www.apache.org/licenses/LICENSE-2.0
+https://www.apache.org/licenses/LICENSE-2.0
This tool is not used in the release binaries.
\ No newline at end of file
diff --git a/README.md b/README.md
index 6d1263678..5da32c6ce 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
StackExchange.Redis
===================
+StackExchange.Redis is a .NET client for communicating with RESP servers such as [Redis](https://redis.io/), [Azure Managed Redis](https://azure.microsoft.com/products/managed-redis), [Garnet](https://microsoft.github.io/garnet/), [Valkey](https://valkey.io/), [AWS ElastiCache](https://aws.amazon.com/elasticache/), and a wide range of other Redis-like servers. We do not maintain a list of compatible servers, but if the server has a Redis-like API: it will *probably* work fine. If not: log an issue with details!
+
For all documentation, [see here](https://stackexchange.github.io/StackExchange.Redis/).
#### Build Status
-[](https://ci.appveyor.com/project/StackExchange/stackexchange-redis/branch/master)
+[](https://github.com/StackExchange/StackExchange.Redis/actions/workflows/CI.yml)
#### Package Status
@@ -13,5 +15,6 @@ MyGet Pre-release feed: https://www.myget.org/gallery/stackoverflow
| Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet |
| ------- | ------------ | ----------------- | --------- | ----- |
-| [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) |
-| [NRediSearch](https://www.nuget.org/packages/NRediSearch/) | [](https://www.nuget.org/packages/NRediSearch/) | [](https://www.nuget.org/packages/NRediSearch/) | [](https://www.nuget.org/packages/NRediSearch/) | [](https://www.myget.org/feed/stackoverflow/package/nuget/NRediSearch) |
+| [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/absoluteLatest) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) |
+
+Release notes at: https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes
diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln
index ca575d8c9..4d275ad4f 100644
--- a/StackExchange.Redis.sln
+++ b/StackExchange.Redis.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28531.58
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31808.319
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AD17044-6BFF-4750-9AC2-2CA466375F2A}"
ProjectSection(SolutionItems) = preProject
@@ -9,14 +9,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
build.cmd = build.cmd
Build.csproj = Build.csproj
build.ps1 = build.ps1
+ .github\workflows\CI.yml = .github\workflows\CI.yml
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
+ Directory.Packages.props = Directory.Packages.props
+ tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml
global.json = global.json
NuGet.Config = NuGet.Config
README.md = README.md
docs\ReleaseNotes.md = docs\ReleaseNotes.md
Shared.ruleset = Shared.ruleset
version.json = version.json
+ tests\RedisConfigs\.docker\Redis\Dockerfile = tests\RedisConfigs\.docker\Redis\Dockerfile
+ .github\workflows\codeql.yml = .github\workflows\codeql.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfigs", "{96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}"
@@ -41,10 +46,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Redis.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTest", "tests\BasicTest\BasicTest.csproj", "{939FA5F7-16AA-4847-812B-6EBC3748A86D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NRediSearch", "src\NRediSearch\NRediSearch.csproj", "{71455B07-E628-4F3A-9FFF-9EC63071F78E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NRediSearch.Test", "tests\NRediSearch.Test\NRediSearch.Test.csproj", "{94D233F5-2400-4542-98B9-BA72005C57DC}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sentinel", "Sentinel", "{36255A0A-89EC-43C8-A642-F4C1ACAEF5BC}"
ProjectSection(SolutionItems) = preProject
tests\RedisConfigs\Sentinel\redis-7010.conf = tests\RedisConfigs\Sentinel\redis-7010.conf
@@ -72,9 +73,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cluster", "Cluster", "{A3B4
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Basic", "Basic", "{38BDEEED-7BEB-4B1F-9CE0-256D63F9C502}"
ProjectSection(SolutionItems) = preProject
- tests\RedisConfigs\Basic\master-6379.conf = tests\RedisConfigs\Basic\master-6379.conf
+ tests\RedisConfigs\Basic\primary-6379.conf = tests\RedisConfigs\Basic\primary-6379.conf
tests\RedisConfigs\Basic\replica-6380.conf = tests\RedisConfigs\Basic\replica-6380.conf
tests\RedisConfigs\Basic\secure-6381.conf = tests\RedisConfigs\Basic\secure-6381.conf
+ tests\RedisConfigs\Basic\tls-ciphers-6384.conf = tests\RedisConfigs\Basic\tls-ciphers-6384.conf
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTestBaseline", "tests\BasicTestBaseline\BasicTestBaseline.csproj", "{8FDB623D-779B-4A84-BC6B-75106E41D8A4}"
@@ -83,7 +85,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsole", "toys\TestCon
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Failover", "Failover", "{D082703F-1652-4C35-840D-7D377F6B9979}"
ProjectSection(SolutionItems) = preProject
- tests\RedisConfigs\Failover\master-6382.conf = tests\RedisConfigs\Failover\master-6382.conf
+ tests\RedisConfigs\Failover\primary-6382.conf = tests\RedisConfigs\Failover\primary-6382.conf
tests\RedisConfigs\Failover\replica-6383.conf = tests\RedisConfigs\Failover\replica-6383.conf
EndProjectSection
EndProject
@@ -93,7 +95,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KestrelRedisServer", "toys\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}"
ProjectSection(SolutionItems) = preProject
- tests\Directory.Build.props = tests\Directory.Build.props
+ tests\.editorconfig = tests\.editorconfig
tests\Directory.Build.targets = tests\Directory.Build.targets
EndProjectSection
EndProject
@@ -104,29 +106,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00CA0876-DA9
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "toys", "toys", "{E25031D3-5C64-430D-B86F-697B66816FD8}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E668-41AD-9E0F-6785CE7EED66}"
- ProjectSection(SolutionItems) = preProject
- docs\Basics.md = docs\Basics.md
- docs\Configuration.md = docs\Configuration.md
- docs\Events.md = docs\Events.md
- docs\ExecSync.md = docs\ExecSync.md
- docs\index.md = docs\index.md
- docs\KeysScan.md = docs\KeysScan.md
- docs\KeysValues.md = docs\KeysValues.md
- docs\PipelinesMultiplexers.md = docs\PipelinesMultiplexers.md
- docs\Profiling.md = docs\Profiling.md
- docs\Profiling_v1.md = docs\Profiling_v1.md
- docs\Profiling_v2.md = docs\Profiling_v2.md
- docs\PubSubOrder.md = docs\PubSubOrder.md
- docs\ReleaseNotes.md = docs\ReleaseNotes.md
- docs\Scripting.md = docs\Scripting.md
- docs\Server.md = docs\Server.md
- docs\Testing.md = docs\Testing.md
- docs\ThreadTheft.md = docs\ThreadTheft.md
- docs\Timeouts.md = docs\Timeouts.md
- docs\Transactions.md = docs\Transactions.md
- EndProjectSection
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsoleBaseline", "toys\TestConsoleBaseline\TestConsoleBaseline.csproj", "{D58114AE-4998-4647-AFCA-9353D20495AE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = ".github", ".github\.github.csproj", "{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}"
@@ -137,11 +116,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{A9F81D
tests\RedisConfigs\Docker\supervisord.conf = tests\RedisConfigs\Docker\supervisord.conf
EndProjectSection
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RediSearch", "RediSearch", "{3FA2A7C6-DA16-4DEF-ACE0-34573A4AD430}"
- ProjectSection(SolutionItems) = preProject
- tests\RedisConfigs\RediSearch\redisearch-6385.conf = tests\RedisConfigs\RediSearch\redisearch-6385.conf
- tests\RedisConfigs\RediSearch\redisearch.md = tests\RedisConfigs\RediSearch\redisearch.md
- EndProjectSection
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTest", "tests\ConsoleTest\ConsoleTest.csproj", "{A0F89B8B-32A3-4C28-8F1B-ADE343F16137}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTestBaseline", "tests\ConsoleTestBaseline\ConsoleTestBaseline.csproj", "{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{1DC43E76-5372-4C7F-A433-0602273E87FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EBD-45F4-808E-3447A293F96F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite", "src\RESPite\RESPite.csproj", "{05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Tests", "tests\RESPite.Tests\RESPite.Tests.csproj", "{CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -161,14 +150,6 @@ Global
{939FA5F7-16AA-4847-812B-6EBC3748A86D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{939FA5F7-16AA-4847-812B-6EBC3748A86D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{939FA5F7-16AA-4847-812B-6EBC3748A86D}.Release|Any CPU.Build.0 = Release|Any CPU
- {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Release|Any CPU.Build.0 = Release|Any CPU
- {94D233F5-2400-4542-98B9-BA72005C57DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {94D233F5-2400-4542-98B9-BA72005C57DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {94D233F5-2400-4542-98B9-BA72005C57DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {94D233F5-2400-4542-98B9-BA72005C57DC}.Release|Any CPU.Build.0 = Release|Any CPU
{8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -193,6 +174,34 @@ Global
{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1DC43E76-5372-4C7F-A433-0602273E87FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1DC43E76-5372-4C7F-A433-0602273E87FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -202,8 +211,6 @@ Global
{EF84877F-59BE-41BE-9013-E765AF0BB72E} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A}
{3B8BD8F1-8BFC-4D8C-B4DA-25FFAF3D1DBE} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{939FA5F7-16AA-4847-812B-6EBC3748A86D} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
- {71455B07-E628-4F3A-9FFF-9EC63071F78E} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A}
- {94D233F5-2400-4542-98B9-BA72005C57DC} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{36255A0A-89EC-43C8-A642-F4C1ACAEF5BC} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
{A3B4B972-5BD2-4D90-981F-7E51E350E628} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
{38BDEEED-7BEB-4B1F-9CE0-256D63F9C502} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
@@ -212,10 +219,14 @@ Global
{D082703F-1652-4C35-840D-7D377F6B9979} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
{8375813E-FBAF-4DA3-A2C7-E4645B39B931} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{3DA1EEED-E9FE-43D9-B293-E000CFCCD91A} = {E25031D3-5C64-430D-B86F-697B66816FD8}
- {153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A}
{D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
- {3FA2A7C6-DA16-4DEF-ACE0-34573A4AD430} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
+ {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
+ {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
+ {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
+ {190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F}
+ {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A}
+ {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B}
diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings
index 165f8337f..8dd9095d9 100644
--- a/StackExchange.Redis.sln.DotSettings
+++ b/StackExchange.Redis.sln.DotSettings
@@ -1,3 +1,29 @@
OK
- PONG
\ No newline at end of file
+ PONG
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
index 2cbf38d46..678032414 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -6,17 +6,17 @@ init:
install:
- cmd: >-
- choco install dotnet-sdk --version 5.0.100
+ choco install dotnet-9.0-sdk
cd tests\RedisConfigs\3.0.503
- redis-server.exe --service-install --service-name "redis-6379" "..\Basic\master-6379.conf"
+ redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379-3.0.conf"
redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf"
redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf"
- redis-server.exe --service-install --service-name "redis-6382" "..\Failover\master-6382.conf"
+ redis-server.exe --service-install --service-name "redis-6382" "..\Failover\primary-6382.conf"
redis-server.exe --service-install --service-name "redis-6383" "..\Failover\replica-6383.conf"
@@ -48,6 +48,10 @@ install:
Start-Service redis-*
}
+branches:
+ only:
+ - main
+
skip_branch_with_pr: true
skip_tags: true
skip_commits:
@@ -63,11 +67,12 @@ nuget:
disable_publish_on_pr: true
build_script:
-- ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages ($env:OS -eq "Windows_NT")
+- ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages ($env:OS -eq "Windows_NT") -NetCoreOnlyTests
test: off
artifacts:
- path: .\.nupkgs\*.nupkg
+- path: '**\*.trx'
deploy:
- provider: NuGet
diff --git a/build.ps1 b/build.ps1
index 24152baab..3ace75a06 100644
--- a/build.ps1
+++ b/build.ps1
@@ -3,7 +3,8 @@ param(
[bool] $CreatePackages,
[switch] $StartServers,
[bool] $RunTests = $true,
- [string] $PullRequestNumber
+ [string] $PullRequestNumber,
+ [switch] $NetCoreOnlyTests
)
Write-Host "Run Parameters:" -ForegroundColor Cyan
@@ -29,7 +30,11 @@ if ($RunTests) {
Write-Host "Servers Started." -ForegroundColor "Green"
}
Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta"
- dotnet test ".\Build.csproj" -c Release --no-build --logger trx
+ if ($NetCoreOnlyTests) {
+ dotnet test ".\Build.csproj" -c Release -f net8.0 --no-build --logger trx
+ } else {
+ dotnet test ".\Build.csproj" -c Release --no-build --logger trx
+ }
if ($LastExitCode -ne 0) {
Write-Host "Error with tests, aborting build." -Foreground "Red"
Exit 1
diff --git a/docs/AsyncTimeouts.md b/docs/AsyncTimeouts.md
new file mode 100644
index 000000000..04892d59a
--- /dev/null
+++ b/docs/AsyncTimeouts.md
@@ -0,0 +1,79 @@
+# Async timeouts and cancellation
+
+StackExchange.Redis directly supports timeout of *synchronous* operations, but for *asynchronous* operations, it is recommended
+to use the inbuilt framework support for cancellation and timeouts, i.e. the [WaitAsync](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.waitasync)
+family of methods. This allows the caller to control timeout (via `TimeSpan`), cancellation (via `CancellationToken`), or both.
+
+Note that it is possible that operations will still be buffered and may still be issued to the server *after* timeout/cancellation means
+that the caller isn't observing the result.
+
+## Usage
+
+### Timeout
+
+Timeouts are probably the most common cancellation scenario:
+
+```csharp
+var timeout = TimeSpan.FromSeconds(5);
+await database.StringSetAsync("key", "value").WaitAsync(timeout);
+var value = await database.StringGetAsync("key").WaitAsync(timeout);
+```
+
+### Cancellation
+
+You can also use `CancellationToken` to drive cancellation, identically:
+
+```csharp
+CancellationToken token = ...; // for example, from HttpContext.RequestAborted
+await database.StringSetAsync("key", "value").WaitAsync(token);
+var value = await database.StringGetAsync("key").WaitAsync(token);
+```
+### Combined Cancellation and Timeout
+
+These two concepts can be combined so that if either cancellation or timeout occur, the caller's
+operation is cancelled:
+
+```csharp
+var timeout = TimeSpan.FromSeconds(5);
+CancellationToken token = ...; // for example, from HttpContext.RequestAborted
+await database.StringSetAsync("key", "value").WaitAsync(timeout, token);
+var value = await database.StringGetAsync("key").WaitAsync(timeout, token);
+```
+
+### Creating a timeout for multiple operations
+
+If you want a timeout to apply to a *group* of operations rather than individually, then you
+can using `CancellationTokenSource` to create a `CancellationToken` that is cancelled after a
+specified timeout. For example:
+
+```csharp
+var timeout = TimeSpan.FromSeconds(5);
+using var cts = new CancellationTokenSource(timeout);
+await database.StringSetAsync("key", "value").WaitAsync(cts.Token);
+var value = await database.StringGetAsync("key").WaitAsync(cts.Token);
+```
+
+This can additionally be combined with one-or-more cancellation tokens:
+
+```csharp
+var timeout = TimeSpan.FromSeconds(5);
+CancellationToken token = ...; // for example, from HttpContext.RequestAborted
+using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); // or multiple tokens
+cts.CancelAfter(timeout);
+await database.StringSetAsync("key", "value").WaitAsync(cts.Token);
+var value = await database.StringGetAsync("key").WaitAsync(cts.Token);
+```
+
+### Cancelling keys enumeration
+
+Keys being enumerated (via `SCAN`) can *also* be cancelled, using the inbuilt `.WithCancellation(...)` method:
+
+```csharp
+CancellationToken token = ...; // for example, from HttpContext.RequestAborted
+await foreach (var key in server.KeysAsync(pattern: "*foo*").WithCancellation(token))
+{
+ ...
+}
+```
+
+To use a timeout instead, you can use the `CancellationTokenSource` approach shown above.
\ No newline at end of file
diff --git a/docs/Authentication.md b/docs/Authentication.md
new file mode 100644
index 000000000..15a673d19
--- /dev/null
+++ b/docs/Authentication.md
@@ -0,0 +1,129 @@
+# Authentication
+
+There are multiple ways of connecting to a Redis server, depending on the authentication model. The simplest
+(but least secure) approach is to use the `default` user, with no authentication, and no transport security.
+This is as simple as:
+
+``` csharp
+var muxer = await ConnectionMultiplexer.ConnectAsync("myserver"); // or myserver:1241 to use a custom port
+```
+
+This approach is often used for local transient servers - it is simple, but insecure. But from there,
+we can get more complex!
+
+## TLS
+
+If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (Azure Managed Redis, etc), the
+library will recognize the endpoint address, meaning: *you do not need to do anything*. To
+*manually* enable TLS, the `ssl` token can be used:
+
+``` csharp
+var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true");
+```
+
+This will work fine if the server is using a server-certificate that is already trusted by the local
+machine. If this is *not* the case, we need to tell the library about the server. This requires
+the `ConfigurationOptions` type:
+
+``` csharp
+var options = ConfigurationOptions.Parse("myserver,ssl=true");
+// or: var options = new ConfigurationOptions { Endpoints = { "myserver" }, Ssl = true };
+// TODO configure
+var muxer = await ConnectionMultiplexer.ConnectAsync(options);
+```
+
+If we have a local *issuer* public certificate (commonly `ca.crt`), we can use:
+
+``` csharp
+options.TrustIssuer(caPath);
+```
+
+Alternatively, in advanced scenarios: to provide your own custom server validation, the `options.CertificateValidation` callback
+can be used; this uses the normal [`RemoteCertificateValidationCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback)
+API.
+
+## Usernames and Passwords
+
+Usernames and passwords can be specified with the `user` and `password` tokens, respectively:
+
+``` csharp
+var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true,user=myuser,password=mypassword");
+```
+
+If no `user` is provided, the `default` user is assumed. In some cases, an authentication-token can be
+used in place of a classic password.
+
+## Managed identities
+
+If the server is an Azure Managed Redis resource, connections can be secured using Microsoft Entra ID authentication. Use the [Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis) extension package to handle the authentication using tokens retrieved from Microsoft Entra. The package integrates via the ConfigurationOptions class, and can use various types of identities for token retrieval. For example with a user-assigned managed identity:
+
+```csharp
+var options = ConfigurationOptions.Parse("mycache.region.redis.azure.net:10000");
+await options.ConfigureForAzureWithUserAssignedManagedIdentityAsync(managedIdentityClientId);
+```
+
+For details and samples see [https://github.com/Azure/Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis)
+
+## Client certificates
+
+If the server is configured to require a client certificate, this can be supplied in multiple ways.
+If you have a local public / private key pair (such as `MyUser2.crt` and `MyUser2.key`), the
+`options.SetUserPemCertificate(...)` method can be used:
+
+``` csharp
+options.SetUserPemCertificate(
+ userCertificatePath: userCrtPath,
+ userKeyPath: userKeyPath
+);
+```
+
+If you have a single `pfx` file that contains the public / private pair, the `options.SetUserPfxCertificate(...)`
+method can be used:
+
+``` csharp
+options.SetUserPfxCertificate(
+ userCertificatePath: userCrtPath,
+ password: filePassword // optional
+);
+```
+
+Alternatively, in advanced scenarios: to provide your own custom client-certificate lookup, the `options.CertificateSelection` callback
+can be used; this uses the normal
+[`LocalCertificateSelectionCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback)
+API.
+
+## User certificates with implicit user authentication
+
+Historically, the client certificate only provided access to the server, but as the `default` user. From 8.6,
+the server can be configured to use client certificates to provide user identity. This replaces the
+usage of passwords, and requires:
+
+- An 8.6+ server, configured to use TLS with client certificates mapped - typically using the `CN` of the certificate as the user.
+- A matching `ACL` user account configured on the server, that is enabled (`on`) - i.e. the `ACL LIST` command should
+ display something like `user MyUser2 on sanitize-payload ~* &* +@all` (the details will vary depending on the user permissions).
+- At the client: access to the client certificate pair.
+
+For example:
+
+``` csharp
+string certRoot = // some path to a folder with ca.crt, MyUser2.crt and MyUser2.key
+
+var options = ConfigurationOptions.Parse("myserver:6380");
+options.SetUserPemCertificate(// automatically enables TLS
+ userCertificatePath: Path.Combine(certRoot, "MyUser2.crt"),
+ userKeyPath: Path.Combine(certRoot, "MyUser2.key"));
+options.TrustIssuer(Path.Combine(certRoot, "ca.crt"));
+await using var conn = await ConnectionMultiplexer.ConnectAsync(options);
+
+// prove we are connected as MyUser2
+var user = (string?)await conn.GetDatabase().ExecuteAsync("acl", "whoami");
+Console.WriteLine(user); // writes "MyUser2"
+```
+
+## More info
+
+For more information:
+
+- [Redis Security](https://redis.io/docs/latest/operate/oss_and_stack/management/security/)
+ - [ACL](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/)
+ - [TLS](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/)
diff --git a/docs/Basics.md b/docs/Basics.md
index 860658458..4d843cb3a 100644
--- a/docs/Basics.md
+++ b/docs/Basics.md
@@ -12,18 +12,18 @@ ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
Note that `ConnectionMultiplexer` implements `IDisposable` and can be disposed when no longer required. This is deliberately not showing `using` statement usage, because it is exceptionally rare that you would want to use a `ConnectionMultiplexer` briefly, as the idea is to re-use this object.
-A more complicated scenario might involve a master/replica setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the master):
+A more complicated scenario might involve a primary/replica setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the primary):
```csharp
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("server1:6379,server2:6379");
```
-If it finds both nodes are masters, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare.
+If it finds both nodes are primaries, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare.
Once you have a `ConnectionMultiplexer`, there are 3 main things you might want to do:
- access a redis database (note that in the case of a cluster, a single logical database may be spread over multiple nodes)
-- make use of the [pub/sub](http://redis.io/topics/pubsub) features of redis
+- make use of the [pub/sub](https://redis.io/topics/pubsub) features of redis
- access an individual server for maintenance / monitoring purposes
Using a redis database
@@ -43,7 +43,7 @@ object asyncState = ...
IDatabase db = redis.GetDatabase(databaseNumber, asyncState);
```
-Once you have the `IDatabase`, it is simply a case of using the [redis API](http://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc.
+Once you have the `IDatabase`, it is simply a case of using the [redis API](https://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc.
The simplest operation would be to store and retrieve a value:
@@ -55,7 +55,7 @@ string value = db.StringGet("mykey");
Console.WriteLine(value); // writes: "abcdefg"
```
-Note that the `String...` prefix here denotes the [String redis type](http://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical:
+Note that the `String...` prefix here denotes the [String redis type](https://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical:
```csharp
byte[] key = ..., value = ...;
@@ -64,18 +64,18 @@ db.StringSet(key, value);
byte[] value = db.StringGet(key);
```
-The entire range of [redis database commands](http://redis.io/commands) covering all redis data types is available for use.
+The entire range of [redis database commands](https://redis.io/commands) covering all redis data types is available for use.
Using redis pub/sub
----
-Another common use of redis is as a [pub/sub message](http://redis.io/topics/pubsub) distribution tool; this is also simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels.
+Another common use of redis is as a [pub/sub message](https://redis.io/topics/pubsub) distribution tool; this is also simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels.
```csharp
ISubscriber sub = redis.GetSubscriber();
```
-Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](http://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message:
+Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](https://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message:
```csharp
sub.Subscribe("messages", (channel, message) => {
@@ -118,13 +118,13 @@ For maintenance purposes, it is sometimes necessary to issue server-specific com
IServer server = redis.GetServer("localhost", 6379);
```
-The `GetServer` method will accept an [`EndPoint`](http://msdn.microsoft.com/en-us/library/system.net.endpoint(v=vs.110).aspx) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available:
+The `GetServer` method will accept an [`EndPoint`](https://docs.microsoft.com/en-us/dotnet/api/system.net.endpoint) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available:
```csharp
EndPoint[] endpoints = redis.GetEndPoints();
```
-From the `IServer` instance, the [Server commands](http://redis.io/commands#server) are available; for example:
+From the `IServer` instance, the [Server commands](https://redis.io/commands#server) are available; for example:
```csharp
DateTime lastSave = server.LastSave();
@@ -139,7 +139,7 @@ There are 3 primary usage mechanisms with StackExchange.Redis:
- Synchronous - where the operation completes before the methods returns to the caller (note that while this may block the caller, it absolutely **does not** block other threads: the key idea in StackExchange.Redis is that it aggressively shares the connection between concurrent callers)
- Asynchronous - where the operation completes some time in the future, and a `Task` or `Task` is returned immediately, which can later:
- be `.Wait()`ed (blocking the current thread until the response is available)
- - have a continuation callback added ([`ContinueWith`](http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.continuewith(v=vs.110).aspx) in the TPL)
+ - have a continuation callback added ([`ContinueWith`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith) in the TPL)
- be *awaited* (which is a language-level feature that simplifies the latter, while also continuing immediately if the reply is already known)
- Fire-and-Forget - where you really aren't interested in the reply, and are happy to continue irrespective of the response
@@ -161,9 +161,6 @@ The fire-and-forget usage is accessed by the optional `CommandFlags flags` param
db.StringIncrement(pageKey, flags: CommandFlags.FireAndForget);
```
-
-
-
- [1]: http://msdn.microsoft.com/en-us/library/dd460717%28v=vs.110%29.aspx
- [2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.asyncstate(v=vs.110).aspx
- [3]: http://msdn.microsoft.com/en-us/library/system.string(v=vs.110).aspx
+ [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
+ [2]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.asyncstate
+ [3]: https://docs.microsoft.com/en-us/dotnet/api/system.string
diff --git a/docs/CompareAndSwap.md b/docs/CompareAndSwap.md
new file mode 100644
index 000000000..7d79d42a0
--- /dev/null
+++ b/docs/CompareAndSwap.md
@@ -0,0 +1,321 @@
+# Compare-And-Swap / Compare-And-Delete (CAS/CAD)
+
+Redis 8.4 introduces atomic Compare-And-Swap (CAS) and Compare-And-Delete (CAD) operations, allowing you to conditionally modify
+or delete values based on their current state. SE.Redis exposes these features through the `ValueCondition` abstraction.
+
+## Prerequisites
+
+- Redis 8.4.0 or later
+
+## Overview
+
+Traditional Redis operations like `SET NX` (set if not exists) and `SET XX` (set if exists) only check for key existence.
+CAS/CAD operations go further by allowing you to verify the **actual value** before making changes, enabling true atomic
+compare-and-swap semantics, without requiring Lua scripts or complex `MULTI`/`WATCH`/`EXEC` usage.
+
+The `ValueCondition` struct supports several condition types:
+
+- **Existence checks**: `Always`, `Exists`, `NotExists` (equivalent to the traditional `When` enum)
+- **Value equality**: `Equal(value)`, `NotEqual(value)` - compare the full value (uses `IFEQ`/`IFNE`)
+- **Digest equality**: `DigestEqual(value)`, `DigestNotEqual(value)` - compare XXH3 64-bit hash (uses `IFDEQ`/`IFDNE`)
+
+## Basic Value Equality Checks
+
+Use value equality when you need to verify the exact current value before updating or deleting:
+
+```csharp
+var db = connection.GetDatabase();
+var key = "user:session:12345";
+
+// Set a value only if it currently equals a specific value
+var currentToken = "old-token-abc";
+var newToken = "new-token-xyz";
+
+var wasSet = await db.StringSetAsync(
+ key,
+ newToken,
+ when: ValueCondition.Equal(currentToken)
+);
+
+if (wasSet)
+{
+ Console.WriteLine("Token successfully rotated");
+}
+else
+{
+ Console.WriteLine("Token mismatch - someone else updated it");
+}
+```
+
+### Conditional Delete
+
+Delete a key only if it contains a specific value:
+
+```csharp
+var lockToken = "my-unique-lock-token";
+
+// Only delete if the lock still has our token
+var wasDeleted = await db.StringDeleteAsync(
+ "resource:lock",
+ when: ValueCondition.Equal(lockToken)
+);
+
+if (wasDeleted)
+{
+ Console.WriteLine("Lock released successfully");
+}
+else
+{
+ Console.WriteLine("Lock was already released or taken by someone else");
+}
+```
+
+(see also the [Lock Operations section](#lock-operations) below)
+
+## Digest-Based Checks
+
+For large values, comparing the full value can be inefficient. Digest-based checks use XXH3 64-bit hashing to compare values efficiently:
+
+```csharp
+var key = "document:content";
+var largeDocument = GetLargeDocumentBytes(); // e.g., 10MB
+
+// Calculate digest locally
+var expectedDigest = ValueCondition.CalculateDigest(largeDocument);
+
+// Update only if the document hasn't changed
+var newDocument = GetUpdatedDocumentBytes();
+var wasSet = await db.StringSetAsync(
+ key,
+ newDocument,
+ when: expectedDigest
+);
+```
+
+### Retrieving Server-Side Digests
+
+You can retrieve the digest of a value stored in Redis without fetching the entire value:
+
+```csharp
+// Get the digest of the current value
+var digest = await db.StringDigestAsync(key);
+
+if (digest.HasValue)
+{
+ Console.WriteLine($"Current digest: {digest.Value}");
+
+ // Later, use this digest for conditional operations
+ var wasDeleted = await db.StringDeleteAsync(key, when: digest.Value);
+}
+else
+{
+ Console.WriteLine("Key does not exist");
+}
+```
+
+## Negating Conditions
+
+Use the `!` operator to negate any condition:
+
+```csharp
+var expectedValue = "old-value";
+
+// Set only if the value is NOT equal to expectedValue
+var wasSet = await db.StringSetAsync(
+ key,
+ "new-value",
+ when: !ValueCondition.Equal(expectedValue)
+);
+
+// Equivalent to:
+var wasSet2 = await db.StringSetAsync(
+ key,
+ "new-value",
+ when: ValueCondition.NotEqual(expectedValue)
+);
+```
+
+## Converting Between Value and Digest Conditions
+
+Convert a value condition to a digest condition for efficiency:
+
+```csharp
+var valueCondition = ValueCondition.Equal("some-value");
+
+// Convert to digest-based check
+var digestCondition = valueCondition.AsDigest();
+
+// Now uses IFDEQ instead of IFEQ
+var wasSet = await db.StringSetAsync(key, "new-value", when: digestCondition);
+```
+
+## Parsing Digests
+
+If you receive a XXH3 digest as a hex string (e.g., from external systems), you can parse it:
+
+```csharp
+// Parse from hex string
+var digestCondition = ValueCondition.ParseDigest("e34615aade2e6333");
+
+// Use in conditional operations
+var wasSet = await db.StringSetAsync(key, newValue, when: digestCondition);
+```
+
+## Lock Operations
+
+StackExchange.Redis automatically uses CAS/CAD for lock operations when Redis 8.4+ is available, providing better performance and atomicity:
+
+```csharp
+var lockKey = "resource:lock";
+var lockToken = Guid.NewGuid().ToString();
+var lockExpiry = TimeSpan.FromSeconds(30);
+
+// Take a lock (uses NX internally)
+if (await db.LockTakeAsync(lockKey, lockToken, lockExpiry))
+{
+ try
+ {
+ // Do work while holding the lock
+
+ // Extend the lock (uses CAS internally on Redis 8.4+)
+ if (!(await db.LockExtendAsync(lockKey, lockToken, lockExpiry)))
+ {
+ // Failed to extend the lock - it expired, or was forcibly taken against our will
+ throw new InvalidOperationException("Lock extension failed - check expiry duration is appropriate.");
+ }
+
+ // Do more work...
+ }
+ finally
+ {
+ // Release the lock (uses CAD internally on Redis 8.4+)
+ await db.LockReleaseAsync(lockKey, lockToken);
+ }
+}
+```
+
+On Redis 8.4+, `LockExtend` uses `SET` with `IFEQ` and `LockRelease` uses `DELEX` with `IFEQ`, eliminating
+the need for transactions.
+
+## Common Patterns
+
+### Optimistic Locking
+
+Implement optimistic concurrency control for updating data:
+
+```csharp
+async Task UpdateUserProfileAsync(string userId, Func updateFunc)
+{
+ var key = $"user:profile:{userId}";
+
+ // Read current value
+ var currentJson = await db.StringGetAsync(key);
+ if (currentJson.IsNull)
+ {
+ return false; // User doesn't exist
+ }
+
+ var currentProfile = JsonSerializer.Deserialize(currentJson!);
+ var updatedProfile = updateFunc(currentProfile);
+ var updatedJson = JsonSerializer.Serialize(updatedProfile);
+
+ // Attempt to update only if value hasn't changed
+ var wasSet = await db.StringSetAsync(
+ key,
+ updatedJson,
+ when: ValueCondition.Equal(currentJson)
+ );
+
+ return wasSet; // Returns false if someone else modified it
+}
+
+// Usage with retry logic
+int maxRetries = 10;
+for (int i = 0; i < maxRetries; i++)
+{
+ if (await UpdateUserProfileAsync(userId, profile =>
+ {
+ profile.LastLogin = DateTime.UtcNow;
+ return profile;
+ }))
+ {
+ break; // Success
+ }
+
+ // Retry with exponential backoff
+ await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, i) * 10));
+}
+```
+
+### Session Token Rotation
+
+Safely rotate session tokens with atomic verification:
+
+```csharp
+async Task RotateSessionTokenAsync(string sessionId, string expectedToken)
+{
+ var key = $"session:{sessionId}";
+ var newToken = GenerateSecureToken();
+
+ // Only rotate if the current token matches
+ var wasRotated = await db.StringSetAsync(
+ key,
+ newToken,
+ expiry: TimeSpan.FromHours(24),
+ when: ValueCondition.Equal(expectedToken)
+ );
+
+ return wasRotated;
+}
+```
+
+### Large Document Updates with Digest
+
+For large documents, use digests to avoid transferring the full value:
+
+```csharp
+async Task UpdateLargeDocumentAsync(string docId, byte[] newContent)
+{
+ var key = $"document:{docId}";
+
+ // Get just the digest, not the full document
+ var currentDigest = await db.StringDigestAsync(key);
+
+ if (!currentDigest.HasValue)
+ {
+ return false; // Document doesn't exist
+ }
+
+ // Update only if digest matches (document unchanged)
+ var wasSet = await db.StringSetAsync(
+ key,
+ newContent,
+ when: currentDigest.Value
+ );
+
+ return wasSet;
+}
+```
+
+## Performance Considerations
+
+### Value vs. Digest Checks
+
+- **Value equality** (`IFEQ`/`IFNE`): Best for small values (< 1KB). Sends the full value to Redis for comparison.
+- **Digest equality** (`IFDEQ`/`IFDNE`): Best for large values. Only sends a 16-character hex digest (8 bytes).
+
+```csharp
+// For small values (session tokens, IDs, etc.)
+var condition = ValueCondition.Equal(smallValue);
+
+// For large values (documents, images, etc.)
+var condition = ValueCondition.DigestEqual(largeValue);
+// or
+var condition = ValueCondition.CalculateDigest(largeValueBytes);
+```
+
+## See Also
+
+- [Transactions](Transactions.md) - For multi-key atomic operations
+- [Keys and Values](KeysValues.md) - Understanding Redis data types
+- [Redis CAS/CAD Documentation](https://redis.io/docs/latest/commands/set/) - Redis 8.4 SET command with IFEQ/IFNE/IFDEQ/IFDNE modifiers
diff --git a/docs/Configuration.md b/docs/Configuration.md
index f73122608..96e4b5bae 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,6 +1,7 @@
-Configuration
+# Configuration
===
+When connecting to Redis version 6 or above with an ACL configured, your ACL user needs to at least have permissions to run the ECHO command. We run this command to verify that we have a valid connection to the Redis service.
Because there are lots of different ways to configure redis, StackExchange.Redis offers a rich configuration model, which is invoked when calling `Connect` (or `ConnectAsync`):
```csharp
@@ -14,7 +15,7 @@ The `configuration` here can be either:
The latter is *basically* a tokenized form of the former.
-Basic Configuration Strings
+## Basic Configuration Strings
-
The *simplest* configuration example is just the host name:
@@ -30,11 +31,11 @@ var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,allowAdmin=tru
```
If you specify a serviceName in the connection string, it will trigger sentinel mode. This example will connect to a sentinel server on the local machine
-using the default sentinel port (26379), discover the current master server for the `mymaster` service and return a managed connection
-pointing to that master server that will automatically be updated if the master changes:
+using the default sentinel port (26379), discover the current primary server for the `myprimary` service and return a managed connection
+pointing to that primary server that will automatically be updated if the primary changes:
```csharp
-var conn = ConnectionMultiplexer.Connect("localhost,serviceName=mymaster");
+var conn = ConnectionMultiplexer.Connect("localhost,serviceName=myprimary");
```
An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially:
@@ -65,7 +66,7 @@ Microsoft Azure Redis example with password
var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=true,password=...");
```
-Configuration Options
+## Configuration Options
---
The `ConfigurationOptions` object has a wide range of properties, all of which are fully documented in intellisense. Some of the more common options to use include:
@@ -75,36 +76,64 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| abortConnect={bool} | `AbortOnConnectFail` | `true` (`false` on Azure) | If true, `Connect` will not create a connection while no servers are available |
| allowAdmin={bool} | `AllowAdmin` | `false` | Enables a range of commands that are considered risky |
| channelPrefix={string} | `ChannelPrefix` | `null` | Optional channel prefix for all pub/sub operations |
-| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. |
+| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. |
| connectRetry={int} | `ConnectRetry` | `3` | The number of times to repeat connect attempts during initial `Connect` |
| connectTimeout={int} | `ConnectTimeout` | `5000` | Timeout (ms) for connect operations |
| configChannel={string} | `ConfigurationChannel` | `__Booksleeve_MasterChanged` | Broadcast channel name for communicating configuration changes |
-| configCheckSeconds={int} | `ConfigCheckSeconds` | `60` | Time (seconds) to check configuration. This serves as a keep-alive for interactive sockets, if it is supported. |
+| configCheckSeconds={int} | `ConfigCheckSeconds` | `60` | Time (seconds) to check configuration. This serves as a keep-alive for interactive sockets, if it is supported. |
| defaultDatabase={int} | `DefaultDatabase` | `null` | Default database index, from `0` to `databases - 1` |
| keepAlive={int} | `KeepAlive` | `-1` | Time (seconds) at which to send a message to help keep sockets alive (60 sec default) |
| name={string} | `ClientName` | `null` | Identification for the connection within redis |
| password={string} | `Password` | `null` | Password for the redis server |
| user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) |
-| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy" |
+| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
-| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel master service |
+| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service |
| ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used |
| sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate |
| sslProtocols={enum} | `SslProtocols` | `null` | Ssl/Tls versions supported when using an encrypted connection. Use '\|' to provide multiple values. |
| syncTimeout={int} | `SyncTimeout` | `5000` | Time (ms) to allow for synchronous operations |
-| asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations |
-| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous master scenario |
-| version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) |
-| | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. |
+| asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations |
+| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario |
+| version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) |
+| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) |
+| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection |
+| protocol={string} | `Protocol` | `null` | Redis protocol to use; see section below |
+| highIntegrity={bool} | `HighIntegrity` | `false` | High integrity (incurs overhead) sequence checking on every command; see section below |
Additional code-only options:
-- ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = LinearRetry(ConnectTimeout);`
+- LoggerFactory (`ILoggerFactory`) - Default: `null`
+ - The logger to use for connection events (not per command), e.g. connection log, disconnects, reconnects, server errors.
+- ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);`
+ - Determines how often a multiplexer will try to reconnect after a failure
+- BacklogPolicy - Default: `BacklogPolicy = BacklogPolicy.Default;`
+ - Determines how commands will be queued (or not) during a disconnect, for sending when it's available again
+- BeforeSocketConnect - Default: `null`
+ - Allows modifying a `Socket` before connecting (for advanced scenarios)
+- SslClientAuthenticationOptions (`netcoreapp3.1`/`net5.0` and higher) - Default: `null`
+ - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receives the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting.
+- SocketManager - Default: `SocketManager.Shared`:
+ - The thread pool to use for scheduling work to and from the socket connected to Redis, one of...
+ - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios.
+ - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play.
+- HighIntegrity - Default: `false`
+ - This enables sending a sequence check command after _every single command_ sent to Redis. This is an opt-in option that incurs overhead to add this integrity check which isn't in the Redis protocol (RESP2/3) itself. The impact on this for a given workload depends on the number of commands, size of payloads, etc. as to how proportionately impactful it will be - you should test with your workloads to assess this.
+ - This is especially relevant if your primary use case is all strings (e.g. key/value caching) where the protocol would otherwise not error.
+ - Intended for cases where network drops (e.g. bytes from the Redis stream, not packet loss) are suspected and integrity of responses is critical.
+- HeartbeatConsistencyChecks - Default: `false`
+ - Allows _always_ sending keepalive checks even if a connection isn't idle. This trades extra commands (per `HeartbeatInterval` - default 1 second) to check the network stream for consistency. If any data was lost, the result won't be as expected and the connection will be terminated ASAP. This is a check to react to any data loss at the network layer as soon as possible.
+- HeartbeatInterval - Default: `1000ms`
+ - Allows running the heartbeat more often which importantly includes timeout evaluation for async commands. For example if you have a 50ms async command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that.
+ - **Note: heartbeats are not free and that's why the default is 1 second. There is additional overhead to running this more often simply because it does some work each time it fires.**
+- LibraryName - Default: `SE.Redis` (unless a `DefaultOptionsProvider` specifies otherwise)
+ - The library name to use with `CLIENT SETINFO` when setting the library name/version on the connection
Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled.
Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`.
-Obsolete Configuration Options
+## Obsolete Configuration Options
---
+
These options are parsed in connection strings for backwards compatibility (meaning they do not error as invalid), but no longer have any effect.
| Configuration string | `ConfigurationOptions` | Previous Default | Previous Meaning |
@@ -112,10 +141,10 @@ These options are parsed in connection strings for backwards compatibility (mean
| responseTimeout={int} | `ResponseTimeout` | `SyncTimeout` | Time (ms) to decide whether the socket is unhealthy |
| writeBuffer={int} | `WriteBuffer` | `4096` | Size of the output buffer |
-Automatic and Manual Configuration
+## Automatic and Manual Configuration
---
-In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and master/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information:
+In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and primary/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information:
```csharp
ConfigurationOptions config = new ConfigurationOptions
@@ -141,7 +170,8 @@ Which is equivalent to the command string:
```config
redis0:6379,redis1:6380,keepAlive=180,version=2.8.8,$CLIENT=,$CLUSTER=,$CONFIG=,$ECHO=,$INFO=,$PING=
```
-Renaming Commands
+
+## Renaming Commands
---
A slightly unusual feature of redis is that you can disable and/or rename individual commands. As per the previous example, this is done via the `CommandMap`, but instead of passing a `HashSet` to `Create()` (to indicate the available or unavailable commands), you pass a `Dictionary`. All commands not mentioned in the dictionary are assumed to be enabled and not renamed. A `null` or blank value records that the command is disabled. For example:
@@ -164,10 +194,32 @@ The above is equivalent to (in the connection string):
$INFO=,$SELECT=use
```
-Twemproxy
+## Redis Server Permissions
---
-[Twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
+If the user you're connecting to Redis with is limited, it still needs to have certain commands enabled for the StackExchange.Redis to succeed in connecting. The client uses:
+- `AUTH` to authenticate
+- `CLIENT` to set the client name
+- `INFO` to understand server topology/settings
+- `ECHO` for heartbeat.
+- (Optional) `SUBSCRIBE` to observe change events
+- (Optional) `CONFIG` to get/understand settings
+- (Optional) `CLUSTER` to get cluster nodes
+- (Optional) `SENTINEL` only for Sentinel servers
+- (Optional) `GET` to determine tie breakers
+- (Optional) `SET` (_only_ if `INFO` is disabled) to see if we're writable
+
+For example, a common _very_ minimal configuration ACL on the server (non-cluster) would be:
+```bash
+-@all +@pubsub +@read +echo +info
+```
+
+Note that if you choose to disable access to the above commands, it needs to be done via the `CommandMap` and not only the ACL on the server (otherwise we'll attempt the command and fail the handshake). Also, if any of the these commands are disabled, some functionality may be diminished or broken.
+
+## twemproxy
+---
+
+[twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
```csharp
var options = new ConfigurationOptions
@@ -177,21 +229,34 @@ var options = new ConfigurationOptions
};
```
-Tiebreakers and Configuration Change Announcements
+##envoyproxy
+---
+
+[Envoyproxy](https://github.com/envoyproxy/envoy) is a tool that allows to front a redis cluster with a set of proxies, with inbuilt discovery and fault tolerance. The feature-set available to Envoyproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
+```csharp
+var options = new ConfigurationOptions+{
+ EndPoints = { "my-proxy1", "my-proxy2", "my-proxy3" },
+ Proxy = Proxy.Envoyproxy
+};
+```
+
+
+## Tiebreakers and Configuration Change Announcements
---
-Normally StackExchange.Redis will resolve master/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple master nodes (for example, while resetting a node for maintenance it may reappear on the network as a master). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple masters are detected (not including redis cluster, where multiple masters are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* master, so that work is routed correctly.
+Normally StackExchange.Redis will resolve primary/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple primary nodes (for example, while resetting a node for maintenance it may reappear on the network as a primary). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple primaries are detected (not including redis cluster, where multiple primaries are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* primary, so that work is routed correctly.
-Likewise, when the configuration is changed (especially the master/replica configuration), it will be important for connected instances to make themselves aware of the new situation (via `INFO`, `CONFIG`, etc - where available). StackExchange.Redis does this by automatically subscribing to a pub/sub channel upon which such notifications may be sent. For similar reasons, this defaults to `"__Booksleeve_MasterChanged"`.
+Likewise, when the configuration is changed (especially the primary/replica configuration), it will be important for connected instances to make themselves aware of the new situation (via `INFO`, `CONFIG`, etc - where available). StackExchange.Redis does this by automatically subscribing to a pub/sub channel upon which such notifications may be sent. For similar reasons, this defaults to `"__Booksleeve_MasterChanged"`.
Both options can be customized or disabled (set to `""`), via the `.ConfigurationChannel` and `.TieBreaker` configuration properties.
-These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to master/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method.
+These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to primary/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method.
-ReconnectRetryPolicy
+## ReconnectRetryPolicy
---
+
StackExchange.Redis automatically tries to reconnect in the background when the connection is lost for any reason. It keeps retrying until the connection has been restored. It would use ReconnectRetryPolicy to decide how long it should wait between the retries.
-ReconnectRetryPolicy can be linear (default), exponential or a custom retry policy.
+ReconnectRetryPolicy can be exponential (default), linear or a custom retry policy.
Examples:
@@ -214,3 +279,16 @@ config.ReconnectRetryPolicy = new LinearRetry(5000);
//5 5000
//6 5000
```
+
+## Redis protocol
+
+Without specific configuration, StackExchange.Redis will use the RESP2 protocol; this means that pub/sub requires a separate connection to the server. RESP3 is a newer protocol
+(usually, but not always, available on v6 servers and above) which allows (among other changes) pub/sub messages to be communicated on the *same* connection - which can be very
+desirable in servers with a large number of clients. The protocol handshake needs to happen very early in the connection, so *by default* the library does not attempt a RESP3 connection
+unless it has reason to expect it to work.
+
+The library determines whether to use RESP3 by:
+- The `HELLO` command has been disabled: RESP2 is used
+- A protocol *other than* `resp3` or `3` is specified: RESP2 is used
+- A protocol of `resp3` or `3` is specified: RESP3 is attempted (with fallback if it fails)
+- In all other scenarios: RESP2 is used
diff --git a/docs/ExecSync.md b/docs/ExecSync.md
index 2b3409fa2..e4a09ec95 100644
--- a/docs/ExecSync.md
+++ b/docs/ExecSync.md
@@ -1,5 +1,5 @@
The Dangers of Synchronous Continuations
===
-Once, there was more content here; then [a suitably evil workaround was found](http://stackoverflow.com/a/22588431/23354). This page is not
+Once, there was more content here; then [a suitably evil workaround was found](https://stackoverflow.com/a/22588431/23354). This page is not
listed in the index, but remains for your curiosity.
\ No newline at end of file
diff --git a/docs/HotKeys.md b/docs/HotKeys.md
new file mode 100644
index 000000000..5ac7c86f9
--- /dev/null
+++ b/docs/HotKeys.md
@@ -0,0 +1,71 @@
+Hot Keys
+===
+
+The `HOTKEYS` command allows for server-side profiling of CPU and network usage by key. It is available in Redis 8.6 and later.
+
+This command is available via the `IServer.HotKeys*` methods:
+
+``` c#
+// Get the server instance.
+IConnectionMultiplexer muxer = ... // connect to Redis 8.6 or later
+var server = muxer.GetServer(endpoint); // or muxer.GetServer(key)
+
+// Start the capture; you can specify a duration, or manually use the HotKeysStop[Async] method; specifying
+// a duration is recommended, so that the profiler will not be left running in the case of failure.
+// Optional parameters allow you to specify the metrics to capture, the sample ratio, and the key slots to include;
+// by default, all metrics are captured, every command is sampled, and all key slots are included.
+await server.HotKeysStartAsync(duration: TimeSpan.FromSeconds(30));
+
+// Now either do some work ourselves, or await for some other activity to happen:
+await Task.Delay(TimeSpan.FromSeconds(35)); // whatever happens: happens
+
+// Fetch the results; note that this does not stop the capture, and you can fetch the results multiple times
+// either while it is running, or after it has completed - but only a single capture can be active at a time.
+var result = await server.HotKeysGetAsync();
+
+// ...investigate the results...
+
+// Optional: discard the active capture data at the server, if any.
+await server.HotKeysResetAsync();
+```
+
+The `HotKeysResult` class (our `result` value above) contains the following properties:
+
+- `Metrics`: The metrics captured during this profiling session.
+- `TrackingActive`: Indicates whether the capture currently active.
+- `SampleRatio`: Profiling frequency; effectively: measure every Nth command. (also: `IsSampled`)
+- `SelectedSlots`: The key slots active for this profiling session.
+- `CollectionStartTime`: The start time of the capture.
+- `CollectionDuration`: The duration of the capture.
+- `AllCommandsAllSlotsTime`: The total CPU time measured for all commands in all slots, without any sampling or filtering applied.
+- `AllCommandsAllSlotsNetworkBytes`: The total network usage measured for all commands in all slots, without any sampling or filtering applied.
+
+When slot filtering is used, the following properties are also available:
+
+- `AllCommandsSelectedSlotsTime`: The total CPU time measured for all commands in the selected slots.
+- `AllCommandsSelectedSlotsNetworkBytes`: The total network usage measured for all commands in the selected slots.
+
+When slot filtering *and* sampling is used, the following properties are also available:
+
+- `SampledCommandsSelectedSlotsTime`: The total CPU time measured for the sampled commands in the selected slots.
+- `SampledCommandsSelectedSlotsNetworkBytes`: The total network usage measured for the sampled commands in the selected slots.
+
+If CPU metrics were captured, the following properties are also available:
+
+- `TotalCpuTimeUser`: The total user CPU time measured in the profiling session.
+- `TotalCpuTimeSystem`: The total system CPU time measured in the profiling session.
+- `TotalCpuTime`: The total CPU time measured in the profiling session.
+- `CpuByKey`: Hot keys, as measured by CPU activity; for each:
+ - `Key`: The key observed.
+ - `Duration`: The time taken.
+
+If network metrics were captured, the following properties are also available:
+
+- `TotalNetworkBytes`: The total network data measured in the profiling session.
+- `NetworkBytesByKey`: Hot keys, as measured by network activity; for each:
+ - `Key`: The key observed.
+ - `Bytes`: The network activity, in bytes.
+
+Note: to use slot-based filtering, you must be connected to a Redis Cluster instance. The
+`IConnectionMultiplexer.HashSlot(RedisKey)` method can be used to determine the slot for a given key. The key
+can also be used in place of an endpoint when using `GetServer(...)` to get the `IServer` instance for a given key.
diff --git a/docs/KeysValues.md b/docs/KeysValues.md
index 0860ef37c..0a414ce21 100644
--- a/docs/KeysValues.md
+++ b/docs/KeysValues.md
@@ -1,9 +1,9 @@
Keys, Values and Channels
===
-In dealing with redis, there is quite an important distinction between *keys* and *everything else*. A key is the unique name of a piece of data (which could be a String, a List, Hash, or any of the other [redis data types](http://redis.io/topics/data-types)) within a database. Keys are never interpreted as... well, anything: they are simply inert names. Further - when dealing with clustered or sharded systems, it is the key that defines the node (or nodes if there are replicas) that contain this data - so keys are crucial for routing commands.
+In dealing with redis, there is quite an important distinction between *keys* and *everything else*. A key is the unique name of a piece of data (which could be a String, a List, Hash, or any of the other [redis data types](https://redis.io/topics/data-types)) within a database. Keys are never interpreted as... well, anything: they are simply inert names. Further - when dealing with clustered or sharded systems, it is the key that defines the node (or nodes if there are replicas) that contain this data - so keys are crucial for routing commands.
-This contrasts with *values*; values are the *things that you store* against keys - either individually (for String data) or as groups. Values do not affect command routing (caveat: except for [the `SORT` command](http://redis.io/commands/sort) when `BY` or `GET` is specified, but that is *really* complicated to explain). Likewise, values are often *interpreted* by redis for the purposes of an operation:
+This contrasts with *values*; values are the *things that you store* against keys - either individually (for String data) or as groups. Values do not affect command routing (caveat: except for [the `SORT` command](https://redis.io/commands/sort) when `BY` or `GET` is specified, but that is *really* complicated to explain). Likewise, values are often *interpreted* by redis for the purposes of an operation:
- `incr` (and the various similar commands) interpret String values as numeric data
- sorting can interpret values using either numeric or unicode rules
@@ -90,7 +90,7 @@ Channel names for pub/sub are represented by the `RedisChannel` type; this is la
Scripting
---
-[Lua scripting in redis](http://redis.io/commands/EVAL) has two notable features:
+[Lua scripting in redis](https://redis.io/commands/EVAL) has two notable features:
- the inputs must keep keys and values separate (which inside the script become `KEYS` and `ARGV`, respectively)
- the return format is not defined in advance: it is specific to your script
diff --git a/docs/KeyspaceNotifications.md b/docs/KeyspaceNotifications.md
new file mode 100644
index 000000000..d9c4f26a1
--- /dev/null
+++ b/docs/KeyspaceNotifications.md
@@ -0,0 +1,213 @@
+# Redis Keyspace Notifications
+
+Redis keyspace notifications let you monitor operations happening on your Redis keys in real-time. StackExchange.Redis provides a strongly-typed API for subscribing to and consuming these events.
+This could be used for example to implement a cache invalidation strategy.
+
+## Prerequisites
+
+### Redis Configuration
+
+You must [enable keyspace notifications](https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#configuration) in your Redis server config,
+for example:
+
+``` conf
+notify-keyspace-events AKE
+```
+
+- **A** - All event types
+- **K** - Keyspace notifications (`__keyspace@__:`)
+- **E** - Keyevent notifications (`__keyevent@__:`)
+
+The two types of event (keyspace and keyevent) encode the same information, but in different formats.
+To simplify consumption, StackExchange.Redis provides a unified API for both types of event, via the `KeyNotification` type.
+
+### Event Broadcasting in Redis Cluster
+
+Importantly, in Redis Cluster, keyspace notifications are **not** broadcast to all nodes - they are only received by clients connecting to the
+individual node where the keyspace notification originated, i.e. where the key was modified.
+This is different to how regular pub/sub events are handled, where a subscription to a channel on one node will receive events published on any node.
+Clients must explicitly subscribe to the same channel on each node they wish to receive events from, which typically means: every primary node in the cluster.
+To make this easier, StackExchange.Redis provides dedicated APIs for subscribing to keyspace and keyevent notifications that handle this for you.
+
+## Quick Start
+
+As an example, we'll subscribe to all keys with a specific prefix, and print out the key and event type for each notification. First,
+we need to create a `RedisChannel`:
+
+```csharp
+// this will subscribe to __keyspace@0__:user:*, including supporting Redis Cluster
+var channel = RedisChannel.KeySpacePrefix(prefix: "user:"u8, database: 0);
+```
+
+Note that there are a range of other `KeySpace...` and `KeyEvent...` methods for different scenarios, including:
+
+- `KeySpaceSingleKey` - subscribe to notifications for a single key in a specific database
+- `KeySpacePattern` - subscribe to notifications for a key pattern, optionally in a specific database
+- `KeySpacePrefix` - subscribe to notifications for all keys with a specific prefix, optionally in a specific database
+- `KeyEvent` - subscribe to notifications for a specific event type, optionally in a specific database
+
+The `KeySpace*` methods are similar, and are presented separately to make the intent clear. For example, `KeySpacePattern("foo*")` is equivalent to `KeySpacePrefix("foo")`, and will subscribe to all keys beginning with `"foo"`.
+
+Next, we subscribe to the channel and process the notifications using the normal pub/sub subscription API; there are two
+main approaches: queue-based and callback-based.
+
+Queue-based:
+
+```csharp
+var queue = await sub.SubscribeAsync(channel);
+_ = Task.Run(async () =>
+{
+ await foreach (var msg in queue)
+ {
+ if (msg.TryParseKeyNotification(out var notification))
+ {
+ Console.WriteLine($"Key: {notification.GetKey()}");
+ Console.WriteLine($"Type: {notification.Type}");
+ Console.WriteLine($"Database: {notification.Database}");
+ }
+ }
+});
+```
+
+Callback-based:
+
+```csharp
+sub.Subscribe(channel, (recvChannel, recvValue) =>
+{
+ if (KeyNotification.TryParse(recvChannel, recvValue, out var notification))
+ {
+ Console.WriteLine($"Key: {notification.GetKey()}");
+ Console.WriteLine($"Type: {notification.Type}");
+ Console.WriteLine($"Database: {notification.Database}");
+ }
+});
+```
+
+Note that the channels created by the `KeySpace...` and `KeyEvent...` methods cannot be used to manually *publish* events,
+only to subscribe to them. The events are published automatically by the Redis server when keys are modified. If you
+want to simulate keyspace notifications by publishing events manually, you should use regular pub/sub channels that avoid
+the `__keyspace@` and `__keyevent@` prefixes.
+
+## Performance considerations for KeyNotification
+
+The `KeyNotification` struct provides parsed notification data, including (as already shown) the key, event type,
+database, etc. Note that using `GetKey()` will allocate a copy of the key bytes; to avoid allocations,
+you can use `TryCopyKey()` to copy the key bytes into a provided buffer (potentially with `GetKeyByteCount()`,
+`GetKeyMaxCharCount()`, etc in order to size the buffer appropriately). Similarly, `KeyStartsWith()` can be used to
+efficiently check the key prefix without allocating a string. This approach is designed to be efficient for high-volume
+notification processing, and in particular: for use with the alt-lookup (span) APIs that are slowly being introduced
+in various .NET APIs.
+
+For example, with a `ConcurrentDictionary` (for some `T`), you can use `GetAlternateLookup>()`
+to get an alternate lookup API that takes a `ReadOnlySpan` instead of a `string`, and then use `TryCopyKey()` to copy
+the key bytes into a buffer, and then use the alt-lookup API to find the value. This means that we avoid allocating a string
+for the key entirely, and instead just copy the bytes into a buffer. If we consider that commonly a local cache will *not*
+contain the key for the majority of notifications (since they are for cache invalidation), this can be a significant
+performance win.
+
+## Considerations when using database isolation
+
+Database isolation is controlled either via the `ConfigurationOptions.DefaultDatabase` option when connecting to Redis,
+or by using the `GetDatabase(int? db = null)` method to get a specific database instance. Note that the
+`KeySpace...` and `KeyEvent...` APIs may optionally take a database. When a database is specified, subscription will only
+respond to notifications for keys in that database. If a database is not specified, the subscription will respond to
+notifications for keys in all databases. Often, you will want to pass `db.Database` from the `IDatabase` instance you are
+using for your application logic, to ensure that you are monitoring the correct database. When using Redis Cluster,
+this usually means database `0`, since Redis Cluster does not usually support multiple databases.
+
+For example:
+
+- `RedisChannel.KeySpaceSingleKey("foo", 0)` maps to `SUBSCRIBE __keyspace@0__:foo`
+- `RedisChannel.KeySpacePrefix("foo", 0)` maps to `PSUBSCRIBE __keyspace@0__:foo*`
+- `RedisChannel.KeySpacePrefix("foo")` maps to `PSUBSCRIBE __keyspace@*__:foo*`
+- `RedisChannel.KeyEvent(KeyNotificationType.Set, 0)` maps to `SUBSCRIBE __keyevent@0__:set`
+- `RedisChannel.KeyEvent(KeyNotificationType.Set)` maps to `PSUBSCRIBE __keyevent@*__:set`
+
+Additionally, note that while most of these examples require multi-node subscriptions on Redis Cluster, `KeySpaceSingleKey`
+is an exception, and will only subscribe to the single node that owns the key `foo`.
+
+When subscribing without specifying a database (i.e. listening to changes in all database), the database relating
+to the notification can be fetched via `KeyNotification.Database`:
+
+``` c#
+var channel = RedisChannel.KeySpacePrefix("foo");
+sub.SubscribeAsync(channel, (recvChannel, recvValue) =>
+{
+ if (KeyNotification.TryParse(recvChannel, recvValue, out var notification))
+ {
+ var key = notification.GetKey();
+ var db = notification.Database;
+ // ...
+ }
+}
+```
+
+## Considerations when using keyspace or channel isolation
+
+StackExchange.Redis supports the concept of keyspace and channel (pub/sub) isolation.
+
+Channel isolation is controlled using the `ConfigurationOptions.ChannelPrefix` option when connecting to Redis.
+Intentionally, this feature *is ignored* by the `KeySpace...` and `KeyEvent...` APIs, because they are designed to
+subscribe to specific (server-defined) channels that are outside the control of the client.
+
+Keyspace isolation is controlled using the `WithKeyPrefix` extension method on `IDatabase`. This is *not* used
+by the `KeySpace...` and `KeyEvent...` APIs. Since the database and pub/sub APIs are independent, keyspace isolation
+*is not applied* (and cannot be; consuming code could have zero, one, or multiple databases with different prefixes).
+The caller is responsible for ensuring that the prefix is applied appropriately when constructing the `RedisChannel`.
+
+By default, key-related features of `KeyNotification` will return the full key reported by the server,
+including any prefix. However, the `TryParseKeyNotification` and `TryParse` methods can optionally be passed a
+key prefix, which will be used both to filter unwanted notifications and strip the prefix from the key when reading.
+It is *possible* to handle keyspace isolation manually by checking the key with `KeyNotification.KeyStartsWith` and
+manually trimming the prefix, but it is *recommended* to do this via `TryParseKeyNotification` and `TryParse`.
+
+As an example, with a multi-tenant scenario using keyspace isolation, we might have in the database code:
+
+``` c#
+// multi-tenant scenario using keyspace isolation
+byte[] keyPrefix = Encoding.UTF8.GetBytes("client1234:");
+var db = conn.GetDatabase().WithKeyPrefix(keyPrefix);
+
+// we will later commit order data for example:
+await db.StringSetAsync("order/123", "ISBN 9789123684434");
+```
+
+To observe this, we could use:
+
+``` c#
+var sub = conn.GetSubscriber();
+
+// subscribe to the specific tenant as a prefix:
+var channel = RedisChannel.KeySpacePrefix("client1234:order/", db.Database);
+
+sub.SubscribeAsync(channel, (recvChannel, recvValue) =>
+{
+ // by including prefix in the TryParse, we filter out notifications that are not for this client
+ // *and* the key is sliced internally to remove this prefix when reading
+ if (KeyNotification.TryParse(keyPrefix, recvChannel, recvValue, out var notification))
+ {
+ // if we get here, the key prefix was a match
+ var key = notification.GetKey(); // "order/123" - note no prefix
+ // ...
+ }
+
+ /*
+ // for contrast only: this is *not* usually the recommended approach when using keyspace isolation
+ if (KeyNotification.TryParse(recvChannel, recvValue, out var notification)
+ && notification.KeyStartsWith(keyPrefix))
+ {
+ var key = notification.GetKey(); // "client1234:order/123" - note prefix is included
+ // ...
+ }
+ */
+});
+
+```
+
+Alternatively, if we wanted a single handler that observed *all* tenants, we could use:
+
+``` c#
+var channel = RedisChannel.KeySpacePattern("client*:order/*", db.Database);
+```
+
+with similar code, parsing the client from the key manually, using the full key length.
\ No newline at end of file
diff --git a/docs/PipelinesMultiplexers.md b/docs/PipelinesMultiplexers.md
index aa47b2a50..b1711531f 100644
--- a/docs/PipelinesMultiplexers.md
+++ b/docs/PipelinesMultiplexers.md
@@ -69,7 +69,7 @@ Multiplexing
Pipelining is all well and good, but often any single block of code only wants a single value (or maybe wants to perform a few operations, but which depend on each-other). This means that we still have the problem that we spend most of our time waiting for data to transfer between client and server. Now consider a busy application, perhaps a web-server. Such applications are generally inherently concurrent, so if you have 20 parallel application requests all requiring data, you might think of spinning up 20 connections, or you could synchronize access to a single connection (which would mean the last caller would need to wait for the latency of all the other 19 before it even got started). Or as a compromise, perhaps a pool of 5 connections which are leased - no matter how you are doing it, there is going to be a lot of waiting. **StackExchange.Redis does not do this**; instead, it does a *lot* of work for you to make effective use of all this idle time by *multiplexing* a single connection. When used concurrently by different callers, it **automatically pipelines the separate requests**, so regardless of whether the requests use blocking or asynchronous access, the work is all pipelined. So we could have 10 or 20 of our "get a and b" scenario from earlier (from different application requests), and they would all get onto the connection as soon as possible. Essentially, it fills the `waiting` time with work from other callers.
-For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](http://redis.io/commands/blpop), [BRPOP](http://redis.io/commands/brpop) and [BRPOPLPUSH](http://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](Transactions). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead:
+For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](https://redis.io/commands/blpop), [BRPOP](https://redis.io/commands/brpop) and [BRPOPLPUSH](https://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](Transactions). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead:
```csharp
sub.Subscribe(channel, delegate {
@@ -105,6 +105,6 @@ if (value == null) {
return value;
```
- [1]: http://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
- [2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx
- [3]: http://msdn.microsoft.com/en-us/library/dd321424(v=vs.110).aspx
+ [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
+ [2]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task
+ [3]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1
diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index f6ef64307..c4cf50c32 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -1,244 +1,628 @@
# Release Notes
+Current package versions:
+
+| NuGet Stable | NuGet Pre-release | MyGet |
+| ------------ | ----------------- | ----- |
+| [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) |
+
+## Unreleased
+
+- (none)
+
+## 2.12.1
+
+- Add missing `LCS` outputs and missing `RedisType.VectorSet` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
+- Track and report multiplexer count ([#3030 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3030))
+- (docs) Add Entra ID authentication docs ([#3023 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3023))
+- (eng) Improve test infrastructure (toy-server) ([#3021 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3021), [#3022 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3022), [#3027 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3027), [#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
+- (eng) Pre-V2 work: bring RESPite down, toy-server, migrate to `AsciiHash` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
+
+## 2.11.8
+
+* Handle `-MOVED` error pointing to same endpoint. ([#3003 by @barshaul](https://github.com/StackExchange/StackExchange.Redis/pull/3003))
+* fix time conversion error in `HOTKEYS` ([#3017 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3017))
+
+- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011))
+- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013))
+
+## 2.11.3
+
+- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011))
+- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013))
+
+## 2.11.0
+
+- Add support for `HOTKEYS` ([#3008 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3008))
+- Add support for keyspace notifications ([#2995 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2995))
+- Add support for idempotent stream entry (`XADD IDMP[AUTO]`) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006))
+- (internals) split AMR out to a separate options provider ([#2986 by NickCraver and philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2986))
+
+## 2.10.14
+
+- Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/3002))
+- Fix under-count of `TotalOutstanding` in server-counters ([#2996 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/2996))
+- Fix incorrect debug assertion in `HGETEX` (no impact to release library) ([#2999 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2999))
+
+
+## 2.10.1
+
+- Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`)
+ via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978))
+ - **note**: overload resolution for `StringSet[Async]` may be impacted in niche cases, requiring trivial build changes (there are no runtime-breaking changes such as missing methods)
+- Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972))
+- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977))
+
+## 2.9.32
+
+- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969))
+
+## 2.9.25
+
+- (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2963))
+
+## 2.9.24
+
+- Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956))
+- Mitigate [#2955](https://github.com/StackExchange/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) / add `RedisValue.WithKeyRouting()` ([#2958 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2958))
+- Fix envoyproxy command exclusions ([#2957 by sshumakov](https://github.com/StackExchange/StackExchange.Redis/pull/2957))
+- Restrict `RedisValue` hex fallback (`string` conversion) to encoding failures ([2954 by jcaspes](https://github.com/StackExchange/StackExchange.Redis/pull/2954))
+- (internals) prefer `Volatile.Read` over `Thread.VolatileRead` ([2960 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2960))
+
+## 2.9.17
+
+- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939))
+- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950))
+- Internals:
+ - Use `sealed` classes where possible ([#2942 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2942))
+ - Add overlapped flushing in `LoggingTunnel` and avoid double-lookups ([#2943 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2943))
+
+## 2.9.11
+
+- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
+- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
+- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
+- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
+- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))
+- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936))
+- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941))
+
+## 2.8.58
+
+- Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680))
+- Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911))
+- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842))
+- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912))
+- Fix `ZREVRANGEBYLEX` open-ended commands ([#2636 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2636))
+- Fix `StreamGroupInfo.Lag` when `null` ([#2902 by robhop](https://github.com/StackExchange/StackExchange.Redis/pull/2902))
+- Internals
+ - Logging improvements ([#2903 by Meir017](https://github.com/StackExchange/StackExchange.Redis/pull/2903) and [#2917 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2917))
+ - Update tests to xUnit v3 ([#2907 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2907))
+ - Avoid `CLIENT PAUSE` in CI tests ([#2916 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2916))
+
+## 2.8.47
+
+- Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900))
+- Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906))
+- Docs: added [guidance on async timeouts](https://stackexchange.github.io/StackExchange.Redis/AsyncTimeouts) ([#2910 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2910))
+- Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909))
+
+## 2.8.41
+
+- Add support for sharded pub/sub via `RedisChannel.Sharded` - ([#2887 by vandyvilla, atakavci and mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2887))
+
+## 2.8.37
+
+- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873))
+- Add logging for when a Multiplexer reconfigures ([#2864 by st-dev-gh](https://github.com/StackExchange/StackExchange.Redis/pull/2864))
+- Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2878))
+
+## 2.8.31
+
+- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver)
+ - Special thanks to [sampdei](https://github.com/sampdei) tracking this down and working a fix
+- Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853))
+- Fix [#2652](https://github.com/StackExchange/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/StackExchange/StackExchange.Redis/pull/2814))
+
+## 2.8.24
+
+- Update Envoy command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2824))
+
+## 2.8.22
+
+- Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2813))
+- Update default Redis version from `4.0.0` to `6.0.0` for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810))
+- Detect Azure Managed Redis caches and tune default connection settings for them ([#2818 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2818))
+- Bump `Microsoft.Bcl.AsyncInterfaces` dependency from `5.0.0` to `6.0.0` ([#2820 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2820))
+
+## 2.8.16
+
+- Fix: PhysicalBridge: Always perform "last read" check in heartbeat when `HeartbeatConsistencyChecks` is enabled ([#2795 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2795))
+
+## 2.8.14
+
+- Fix [#2793](https://github.com/StackExchange/StackExchange.Redis/issues/2793): Update Envoyproxy's command map according to latest Envoy documentation ([#2794 by dbarbosapn](https://github.com/StackExchange/StackExchange.Redis/pull/2794))
+
+## 2.8.12
+
+- Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716]))
+- Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722))
+- Fix [#2763](https://github.com/StackExchange/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/StackExchange/StackExchange.Redis/pull/2769))
+- Fix [#2778](https://github.com/StackExchange/StackExchange.Redis/issues/2778): Run `CheckInfoReplication` even with `HeartbeatConsistencyChecks` ([#2784 by NickCraver and leachdaniel-clark](https://github.com/StackExchange/StackExchange.Redis/pull/2784))
+
+## 2.8.0
+
+- Add high-integrity mode ([docs](https://stackexchange.github.io/StackExchange.Redis/Configuration), [#2471 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2741))
+- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670))
+
+## 2.7.33
+
+- **Potentially Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665))
+ - Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all.
+- Add new `LoggingTunnel` API; see [https://stackexchange.github.io/StackExchange.Redis/RespLogging](https://stackexchange.github.io/StackExchange.Redis/RespLogging) ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660))
+- Fix [#2664](https://github.com/StackExchange/StackExchange.Redis/issues/2664): Move ProcessBacklog to fully sync to prevent thread pool hopping and blocking on awaits ([#2667 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2667))
+
+## 2.7.27
+
+- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658))
+- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ([#2659 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2659))
+
+## 2.7.23
+
+- Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2654))
+- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occurred ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656))
+
+## 2.7.20
+
+- Fix [#2642](https://github.com/StackExchange/StackExchange.Redis/issues/2642): Detect and support multi-DB pseudo-cluster/proxy scenarios ([#2646](https://github.com/StackExchange/StackExchange.Redis/pull/2646) by mgravell)
+
+## 2.7.17
+
+- Fix [#2321](https://github.com/StackExchange/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/StackExchange/StackExchange.Redis/pull/2322)
+- Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621))
+- `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623))
+- Fix [#2576](https://github.com/StackExchange/StackExchange.Redis/issues/2576): Prevent `NullReferenceException` during shutdown of connections ([#2629 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2629))
+
+## 2.7.10
+
+- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593))
+- Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601))
+- Fix [#2595](https://github.com/StackExchange/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments ([#2610 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2610))
+
+## 2.7.4
+
+- Adds: RESP3 support ([#2396 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2396)) - see https://stackexchange.github.io/StackExchange.Redis/Resp3
+- Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508))
+- Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508))
+- Add: `ConfigurationOptions.LoggerFactory` for logging to an `ILoggerFactory` (e.g. `ILogger`) all connection and error events ([#2051 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2051))
+- Fix [#2467](https://github.com/StackExchange/StackExchange.Redis/issues/2467): Add StreamGroupInfo EntriesRead and Lag ([#2510 by tvdias](https://github.com/StackExchange/StackExchange.Redis/pull/2510))
+
+## 2.6.122
+
+- Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497))
+- Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494))
+- Fix [#2520](https://github.com/StackExchange/StackExchange.Redis/issues/2520): Improve cluster connections in down scenarios by not re-pinging successful nodes ([#2525 by Matiszak](https://github.com/StackExchange/StackExchange.Redis/pull/2525))
+- Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500))
+- Add: `LibraryName` configuration option; allows the library name to be controlled at the individual options level (in addition to the existing controls in `DefaultOptionsProvider`) ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502))
+- Add: `DefaultOptionsProvider.GetProvider` allows lookup of provider by endpoint ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502))
+
+## 2.6.116
+
+- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480))
+- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2481))
+- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456))
+
+## 2.6.111
+
+- Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428))
+- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445))
+- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451))
+- Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448))
+- Adds: Timeout duration to backlog timeout error messages ([#2452 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2452))
+- Adds: `DefaultOptionsProvider.LibraryName` for specifying lib-name passed to `CLIENT SETINFO` in Redis 7.2+ ([#2453 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2453))
+
+## 2.6.104
+
+- Fix [#2412](https://github.com/StackExchange/StackExchange.Redis/issues/2412): Critical (but rare) GC bug that can lead to async tasks never completing if the multiplexer is not held by the consumer ([#2408 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2408))
+- Add: Better error messages (over generic timeout) when commands are backlogged and unable to write to any connection ([#2408 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2408))
+- Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397))
+- Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402))
+- Add: Support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414))
+- Documentation: clarify the meaning of `RedisValue.IsInteger` re [#2418](https://github.com/StackExchange/StackExchange.Redis/issues/2418) ([#2420 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2420))
+
+## 2.6.96
+
+- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script parameters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351))
+- Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367))
+- Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370))
+- Fix [#2376](https://github.com/StackExchange/StackExchange.Redis/issues/2376): Avoid a (rare) deadlock scenario ([#2378 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2378))
+
+## 2.6.90
+
+- Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` ([#2168 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2168))
+- Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238))
+- Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337))
+
+
+## 2.6.86
+
+- Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286))
+- Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288))
+- Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306))
+- Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2300))
+
+## 2.6.80
+
+- Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276))
+- Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2274))
+- Removes: Package dependency (`System.Diagnostics.PerformanceCounter`) ([#2285 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2285))
+
+
+## 2.6.70
+
+- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2267))
+- Fix [#2251](https://github.com/StackExchange/StackExchange.Redis/issues/2251) & [#2265](https://github.com/StackExchange/StackExchange.Redis/issues/2265): Cluster endpoint connections weren't proactively connecting subscriptions in all cases and taking the full connection timeout to complete as a result ([#2268 by iteplov](https://github.com/StackExchange/StackExchange.Redis/pull/2268))
+
+
+## 2.6.66
+
+- Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183))
+- Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161))
+- Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203))
+- Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191))
+- Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217))
+- Fix [#2223](https://github.com/StackExchange/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229))
+- Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170))
+- Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224))
+- Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241))
+- Adds: `ConfigurationOptions.HeartbeatInterval` (**Advanced Setting** - [see docs](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options)) To allow more finite control of the client heartbeat, which encompases how often command timeouts are actually evaluated - still defaults to 1,000 ms ([#2243 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2243))
+- Fix [#1879](https://github.com/StackExchange/StackExchange.Redis/issues/1879): Improve exception message when the wrong password is used ([#2246 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2246))
+- Fix [#2233](https://github.com/StackExchange/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2242))
+
+
+## 2.6.48
+
+- URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177))
+- Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166))
+
+## 2.6.45
+
+- Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2041))
+ - Adds annotations themselves for nullability to everything in the library
+ - Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference)
+ - Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases)
+ - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change.
+- Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064))
+- Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065))
+- Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2076))
+- Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077))
+- Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075))
+- Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078))
+- Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080))
+- Adds: Support for `ZMSCORE` with `.SortedSetScores()`/.`SortedSetScoresAsync()` ([#2082 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2082))
+- Adds: Support for `NX | XX | GT | LT` to `EXPIRE`, `EXPIREAT`, `PEXPIRE`, and `PEXPIREAT` with `.KeyExpire()`/`.KeyExpireAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083))
+- Adds: Support for `EXPIRETIME`, and `PEXPIRETIME` with `.KeyExpireTime()`/`.KeyExpireTimeAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083))
+- Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085))
+- Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087))
+- Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088))
+- Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
+- Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
+- Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090))
+- Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
+- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
+- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095))
+- Fix [#2071](https://github.com/StackExchange/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2098))
+- Fix [#2086](https://github.com/StackExchange/StackExchange.Redis/issues/2086): Correct HashSlot calculations for `XREAD` and `XREADGROUP` commands ([#2093 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2093))
+- Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2104))
+- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105))
+- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110))
+- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111))
+- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` ([#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116))
+- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118))
+- Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133))
+- Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2136))
+- Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143))
+
+## 2.5.61
+
+- Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/StackExchange/StackExchange.Redis/pull/1743))
+- Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023))
+- Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029))
+- Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030))
+- Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031))
+- Fix [#1813](https://github.com/StackExchange/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2042))
+- Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2048))
+- Adds: Most `ConfigurationOptions` changes after `ConnectionMultiplexer` connections will now be respected, e.g. changing a timeout will work and changing a password for auth rotation would be used at the next reconnect ([#2050 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2050))
+ - **Obsolete**: This change also moves `ConnectionMultiplexer.IncludeDetailInExceptions` and `ConnectionMultiplexer.IncludePerformanceCountersInExceptions` to `ConfigurationOptions`. The old properties are `[Obsolete]` proxies that work until 3.0 for compatibility.
+- Adds: Support for `ZRANGESTORE` with `.SortedSetRangeAndStore()`/`.SortedSetRangeAndStoreAsync()` ([#2052 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2052))
+
+## 2.5.43
+
+- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921))
+- Adds: `DefaultOptionsProvider` support for endpoint-based defaults configuration ([#1987 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1987))
+- Adds: Envoy proxy support ([#1989 by rkarthick](https://github.com/StackExchange/StackExchange.Redis/pull/1989))
+- Performance: When `SUBSCRIBE` is disabled, give proper errors and connect faster ([#2001 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2001))
+- Adds: `GET` on `SET` command support (present in Redis 6.2+ - [#2003 by martinekvili](https://github.com/StackExchange/StackExchange.Redis/pull/2003))
+- Performance: Improves concurrent load performance when backlogs are utilized ([#2008 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2008))
+- Stability: Improves cluster connections when `CLUSTER` command is disabled ([#2014 by tylerohlsen](https://github.com/StackExchange/StackExchange.Redis/pull/2014))
+- Logging: Improves connection logging and adds overall timing to it ([#2019 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2019))
+
+## 2.5.27 (prerelease)
+
+- Adds: a backlog/retry mechanism for commands issued while a connection isn't available ([#1912 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1912))
+ - Commands will be queued if a multiplexer isn't yet connected to a Redis server.
+ - Commands will be queued if a connection is lost and then sent to the server when the connection is restored.
+ - All commands queued will only remain in the backlog for the duration of the configured timeout.
+ - To revert to previous behavior, a new `ConfigurationOptions.BacklogPolicy` is available - old behavior is configured via `options.BacklogPolicy = BacklogPolicy.FailFast`. This backlogs nothing and fails commands immediately if no connection is available.
+- Adds: Makes `StreamEntry` constructor public for better unit test experience ([#1923 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1923))
+- Fix: Integer overflow error (issue #1926) with 2GiB+ result payloads ([#1928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1928))
+- Change: Update assumed redis versions to v2.8 or v4.0 in the Azure case ([#1929 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1929))
+- Fix: Profiler showing `EVAL` instead `EVALSHA` ([#1930 by martinpotter](https://github.com/StackExchange/StackExchange.Redis/pull/1930))
+- Performance: Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) ([#1931 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1931))
+- Stability: Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell)
+- Adds: Thread pool work item stats to exception messages to help diagnose contention ([#1964 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1964))
+- Fix/Performance: Overhauls pub/sub implementation for correctness ([#1947 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1947))
+ - Fixes a race in subscribing right after connected
+ - Fixes a race in subscribing immediately before a publish
+ - Fixes subscription routing on clusters (spreading instead of choosing 1 node)
+ - More correctly reconnects subscriptions on connection failures, including to other endpoints
+- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected ([#1985 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1985))
+- Fix: Properly including or excluding key names on some message failures ([#1990 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1990))
+- Fix: Correct return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` ([#1993 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1993))
+
+## 2.2.88
+
+- Change: Connection backoff default is now exponential instead of linear ([#1896 by lolodi](https://github.com/StackExchange/StackExchange.Redis/pull/1896))
+- Adds: Support for `NodeMaintenanceScaleComplete` event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1902))
+
+## 2.2.79
+
+- NRediSearch: Support on json index ([#1808 by AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1808))
+- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields ([#1862 by slorello89 & AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1862))
+- Fix: Potential errors getting socket bytes ([#1836 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1836))
+- Logging: Adds (.NET Version and timestamps) for better debugging ([#1796 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/1796))
+- Adds: `Condition` APIs (transactions), now supports `StreamLengthEqual` and variants ([#1807 by AlphaGremlin](https://github.com/StackExchange/StackExchange.Redis/pull/1807))
+- Adds: Support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` ([#1850 by jjfmarket](https://github.com/StackExchange/StackExchange.Redis/pull/1850))
+- Fix: Potential task/thread exhaustion from the backlog processor ([#1854 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1854))
+- Adds: Support for listening to Azure Maintenance Events ([#1876 by amsoedal](https://github.com/StackExchange/StackExchange.Redis/pull/1876))
+- Adds: `StringGetDelete`/`StringGetDeleteAsync` APIs for Redis `GETDEL` command([#1840 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1840))
+
+## 2.2.62
+
+- Stability: Sentinel potential memory leak fix in OnManagedConnectionFailed handler ([#1710 by alexSatov](https://github.com/StackExchange/StackExchange.Redis/pull/1710))
+- Fix: `GetOutstandingCount` could obscure underlying faults by faulting itself ([#1792 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1792))
+- Fix [#1719](https://github.com/StackExchange/StackExchange.Redis/issues/1791): With backlog messages becoming reordered ([#1779 by TimLovellSmith](https://github.com/StackExchange/StackExchange.Redis/pull/1779))
+
## 2.2.50
-- performance optimization for PING accuracy (#1714 via eduardobr)
-- improvement to reconnect logic (exponential backoff) (#1735 via deepakverma)
-- refresh replica endpoint list on failover (#1684 by laurauzcategui)
-- fix for ReconfigureAsync re-entrancy (caused connection issues) (#1772 by NickCraver)
-- fix for ReconfigureAsync Sentinel race resulting in NoConnectionAvailable when using DemandMaster (#1773 by NickCraver)
-- resolve race in AUTH and other connection reconfigurations (#1759 via TimLovellSmith and NickCraver)
+- Performance: Optimization for PING accuracy ([#1714 by eduardobr](https://github.com/StackExchange/StackExchange.Redis/pull/1714))
+- Fix: Improvement to reconnect logic (exponential backoff) ([#1735 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1735))
+- Adds: Refresh replica endpoint list on failover ([#1684 by laurauzcategui](https://github.com/StackExchange/StackExchange.Redis/pull/1684))
+- Fix: `ReconfigureAsync` re-entrancy (caused connection issues) ([#1772 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1772))
+- Fix: `ReconfigureAsync` Sentinel race resulting in NoConnectionAvailable when using DemandMaster ([#1773 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1773))
+- Stability: Resolve race in AUTH and other connection reconfigurations ([#1759 by TimLovellSmith and NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1759))
## 2.2.4
-- fix ambiguous signature of the new `RPUSHX`/`LPUSHX` methods (#1620)
+- Fix: Ambiguous signature of the new `RPUSHX`/`LPUSHX` methods ([#1620 by stefanloerwald](https://github.com/StackExchange/StackExchange.Redis/pull/1620))
## 2.2.3
-- add .NET 5 target
-- fix mutex race condition (#1585 via arsnyder16)
-- allow `CheckCertificateRevocation` to be controlled via the config string (#1591 via lwlwalker)
-- fix range end-value inversion (#1573 via tombatron)
-- add `ROLE` support (#1551 via zmj)
-- add varadic `RPUSHX`/`LPUSHX` support (#1557 via dmytrohridin)
-- fix server-selection strategy race condition (#1532 via deepakverma)
-- fix sentinel default port (#1525 via ejsmith)
-- fix `Int64` parse scenario (#1568 via arsnyder16)
-- force replication check during failover (via joroda)
-- documentation tweaks (multiple)
-- fix backlog contention issue (#1612, see also #1574 via devbv)
+- Adds: .NET 5 target
+- Fix: Mutex race condition ([#1585 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1585))
+- Adds: `CheckCertificateRevocation` can be controlled via the config string ([#1591 by lwlwalker](https://github.com/StackExchange/StackExchange.Redis/pull/1591))
+- Fix: Range end-value inversion ([#1573 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1573))
+- Adds: `ROLE` support ([#1551 by zmj](https://github.com/StackExchange/StackExchange.Redis/pull/1551))
+- Adds: varadic `RPUSHX`/`LPUSHX` support ([#1557 by dmytrohridin](https://github.com/StackExchange/StackExchange.Redis/pull/1557))
+- Fix: Server-selection strategy race condition ([#1532 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1532))
+- Fix: Sentinel default port ([#1525 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1525))
+- Fix: `Int64` parse scenario ([#1568 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1568))
+- Add: Force replication check during failover ([#1563 by aravindyeduvaka & joroda](https://github.com/StackExchange/StackExchange.Redis/pull/1563))
+- Documentation tweaks (multiple)
+- Fix: Backlog contention issue ([#1612 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1612/), see also [#1574 by devbv](https://github.com/StackExchange/StackExchange.Redis/pull/1574/))
## 2.1.58
-- fix: `[*]SCAN` - fix possible NRE scenario if the iterator is disposed with an incomplete operation in flight
-- fix: `[*]SCAN` - treat the cursor as an opaque value whenever possible, for compatibility with `redis-cluster-proxy`
-- add: `[*]SCAN` - include additional exception data in the case of faults
+- Fix: `[*]SCAN` - fix possible NRE scenario if the iterator is disposed with an incomplete operation in flight
+- Fix: `[*]SCAN` - treat the cursor as an opaque value whenever possible, for compatibility with `redis-cluster-proxy`
+- Adds: `[*]SCAN` - include additional exception data in the case of faults
## 2.1.55
-- identify assembly binding problem on .NET Framework; drops `System.IO.Pipelines` to 4.7.1, and identifies new `System.Buffers` binding failure on 4.7.2
+- Adds: Identification of assembly binding problem on .NET Framework. Drops `System.IO.Pipelines` to 4.7.1, and identifies new `System.Buffers` binding failure on 4.7.2
## 2.1.50
-- add: bind direct to sentinel-managed instances from a configuration string/object (#1431 via ejsmith)
-- add last-delivered-id to `StreamGroupInfo` (#1477 via AndyPook)
-- update naming of replication-related commands to reflect Redis 5 naming (#1488/#945)
-- fix: the `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config (#1460)
-- library updates
+- Adds: Bind directly to sentinel-managed instances from a configuration string/object ([#1431 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1431))
+- Adds: `last-delivered-id` to `StreamGroupInfo` ([#1477 by AndyPook](https://github.com/StackExchange/StackExchange.Redis/pull/1477))
+- Change: Update naming of replication-related commands to reflect Redis 5 naming ([#1488 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/1488) & [#945 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/945))
+- Fix [#1460](https://github.com/StackExchange/StackExchange.Redis/issues/1460): `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config ([#1468 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1468))
+- Library updates
## 2.1.39
-- fix: mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands (#1440)
-- fix: update libs (#1432)
-- fix: timing error on linux (#1433 via pengweiqhca)
-- fix: add `auth` to command-map for sentinal (#1428 via ejsmith)
+- Fix: Mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands ([#1440 by kennygea](https://github.com/StackExchange/StackExchange.Redis/pull/1440))
+- Fix [#1432](https://github.com/StackExchange/StackExchange.Redis/issues/1432): Update dependencies
+- Fix: Timing error on linux ([#1433 by pengweiqhca](https://github.com/StackExchange/StackExchange.Redis/pull/1433))
+- Fix: Add `auth` to command-map for Sentinel ([#1428 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1428))
## 2.1.30
-- fix deterministic builds
+- Build: Fix deterministic builds ([#1420 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1420))
## 2.1.28
-- fix: stability in new sentinel APIs
-- fix: include `SslProtocolos` in `ConfigurationOptions.ToString()` (#1408 via vksampath and Sampath Vuyyuru
-- fix: clarify messaging around disconnected multiplexers (#1396)
-- change: tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable)
-- add: new thread`SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool
-- add: improved counters in/around error messages
-- add: new `User` property on `ConfigurationOptions`
-- build: enable deterministic builds (note: this failed; fixed in 2.1.30)
+- Fix: Stability in new sentinel APIs
+- Fix [#1407](https://github.com/StackExchange/StackExchange.Redis/issues/1407): Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/StackExchange/StackExchange.Redis/pull/1408))
+- Fix: Clarify messaging around disconnected multiplexers ([#1396 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1396))
+- Change: Tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable)
+- Adds: New thread `SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool
+- Adds: Improved counters in/around error messages
+- Adds: New `User` property on `ConfigurationOptions`
+- Build: Enable deterministic builds (note: this failed; fixed in 2.1.30)
## 2.1.0
-- fix: ensure active-message is cleared (#1374 via hamish-omny)
-- add: sentinel support (#1067 via shadim; #692 via lexxdark)
-- add: `IAsyncEnumerable` scanning APIs now supported (#1087)
-- add: new API for use with misbehaving sync-contexts ([more info](https://stackexchange.github.io/StackExchange.Redis/ThreadTheft))
-- add: `TOUCH` support (#1291 via gkorland)
-- add: `Condition` API (transactions) now supports `SortedSetLengthEqual` (#1332 via phosphene47)
-- add: `SocketManager` is now more configurable (#1115, via naile)
-- add: NRediSearch updated in line with JRediSearch (#1267, via tombatron; #1199 via oruchreis)
-- add: support for `CheckCertificatRevocation` configuration (#1234, via BLun78 and V912736)
-- add: more details about exceptions (#1190, via marafiq)
-- add: new stream APIs (#1141 and #1154 via ttingen)
-- add: event-args now mockable (#1326 via n1l)
-- fix: no-op when adding 0 values to a set (#1283 via omeaart)
-- add: support for `LATENCY` and `MEMORY` (#1204)
-- add: support for `HSTRLEN` (#1241 via eitanhs)
-- add: `GeoRadiusResult` is now mockable (#1175 via firenero)
-- fix: various documentation fixes (#1162, #1135, #1203, #1240, #1245, #1159, #1311, #1339, #1336)
-- fix: rare race-condition around exception data (#1342)
-- fix: `ScriptEvaluateAsync` keyspace isolation (#1377 via gliljas)
-- fix: F# compatibility enhancements (#1386)
-- fix: improved `ScriptResult` null support (#1392)
-- fix: error with DNS resolution breaking endpoint iterator (#1393)
-- tests: better docker support for tests (#1389 via ejsmith; #1391)
-- tests: general test improvements (#1183, #1385, #1384)
+- Fix: Ensure active-message is cleared ([#1374 by hamish-omny](https://github.com/StackExchange/StackExchange.Redis/pull/1374))
+- Adds: Sentinel support ([#1067 by shadim](https://github.com/StackExchange/StackExchange.Redis/pull/1067), [#692 by lexxdark](https://github.com/StackExchange/StackExchange.Redis/pull/692))
+- Adds: `IAsyncEnumerable` scanning APIs now supported ([#1087 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1087))
+- Adds: New API for use with misbehaving sync-contexts ([more info](https://stackexchange.github.io/StackExchange.Redis/ThreadTheft))
+- Adds: `TOUCH` support ([#1291 by gkorland](https://github.com/StackExchange/StackExchange.Redis/pull/1291))
+- Adds: `Condition` API (transactions) now supports `SortedSetLengthEqual` ([#1332 by phosphene47](https://github.com/StackExchange/StackExchange.Redis/pull/1332))
+- Adds: `SocketManager` is now more configurable ([#1115 by naile](https://github.com/StackExchange/StackExchange.Redis/pull/1115))
+- Adds: NRediSearch updated in line with JRediSearch ([#1267 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1267), [#1199 by oruchreis](https://github.com/StackExchange/StackExchange.Redis/pull/1199))
+- Adds: Support for `CheckCertificatRevocation` configuration ([#1234 by BLun78 and V912736](https://github.com/StackExchange/StackExchange.Redis/pull/1234))
+- Adds: More details about exceptions ([#1190 by marafiq](https://github.com/StackExchange/StackExchange.Redis/pull/1190))
+- Adds: Updated `StreamCreateConsumerGroup` methods to use the `MKSTREAM` option ([#1141 via ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1141))
+- Adds: Support for NOACK in the StreamReadGroup methods ([#1154 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1154))
+- Adds: Event-args now mockable ([#1326 by n1l](https://github.com/StackExchange/StackExchange.Redis/pull/1326))
+- Fix: No-op when adding 0 values to a set ([#1283 by omeaart](https://github.com/StackExchange/StackExchange.Redis/pull/1283))
+- Adds: Support for `LATENCY` and `MEMORY` ([#1204 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1204))
+- Adds: Support for `HSTRLEN` ([#1241 by eitanhs](https://github.com/StackExchange/StackExchange.Redis/pull/1241))
+- Adds: `GeoRadiusResult` is now mockable ([#1175 by firenero](https://github.com/StackExchange/StackExchange.Redis/pull/1175))
+- Fix: Various documentation fixes ([#1162 by SnakyBeaky](https://github.com/StackExchange/StackExchange.Redis/pull/1162), [#1135 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1135), [#1203 by caveman-dick](https://github.com/StackExchange/StackExchange.Redis/pull/1203), [#1240 by Excelan](https://github.com/StackExchange/StackExchange.Redis/pull/1240), [#1245 by francoance](https://github.com/StackExchange/StackExchange.Redis/pull/1245), [#1159 by odyth](https://github.com/StackExchange/StackExchange.Redis/pull/1159), [#1311 by DillonAd](https://github.com/StackExchange/StackExchange.Redis/pull/1311), [#1339 by vp89](https://github.com/StackExchange/StackExchange.Redis/pull/1339), [#1336 by ERGeorgiev](https://github.com/StackExchange/StackExchange.Redis/issues/1336))
+- Fix: Rare race-condition around exception data ([#1342 by AdamOutcalt](https://github.com/StackExchange/StackExchange.Redis/pull/1342))
+- Fix: `ScriptEvaluateAsync` keyspace isolation ([#1377 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1377))
+- Fix: F# compatibility enhancements ([#1386 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1386))
+- Fix: Improved `ScriptResult` null support ([#1392 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1392))
+- Fix: Error with DNS resolution breaking endpoint iterator ([#1393 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1393))
+- Tests: Better docker support for tests ([#1389 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1389), [#1391 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1391))
+- Tests: General test improvements ([#1183 by mtreske](https://github.com/StackExchange/StackExchange.Redis/issues/1183), [#1385 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1385), [#1384 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1384))
## 2.0.601
-- add: tracking for current and next messages to help with debugging timeout issues - helpful in cases of large pipeline blockers
+- Adds: Tracking for current and next messages to help with debugging timeout issues - helpful in cases of large pipeline blockers
## 2.0.600
-- add: `ulong` support to `RedisValue` and `RedisResult` (#1103)
-- fix: remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to #1103)
-- performance: rework how pub/sub queues are stored - reduces delegate overheads (related to #1101)
-- fix #1108 - ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one
+- Adds: `ulong` support to `RedisValue` and `RedisResult` ([#1104 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1104))
+- Fix: Remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to [#1103](https://github.com/StackExchange/StackExchange.Redis/issues/1103))
+- Performance: Rework how pub/sub queues are stored - reduces delegate overheads (related to [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101))
+- Fix [#1108](https://github.com/StackExchange/StackExchange.Redis/issues/1108): Ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one
## 2.0.593
-- performance: unify spin-wait usage on sync/async paths to one competitor
-- fix #1101 - when a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete
+- Performance: Unify spin-wait usage on sync/async paths to one competitor
+- Fix [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101): When a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete
## 2.0.588
-- stability and performance: resolve intermittent stall in the write-lock that could lead to unexpected timeouts even when at low/reasonable (but concurrent) load
+- Stability/Performance: Resolve intermittent stall in the write-lock that could lead to unexpected timeouts even when at low/reasonable (but concurrent) load
## 2.0.571
-- performance: use new [arena allocation API](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas) to avoid `RawResult[]` overhead
-- performance: massively simplified how `ResultBox` is implemented, in particular to reduce `TaskCompletionSource` allocations
-- performance: fix sync-over-async issue with async call paths, and fix the [SemaphoreSlim](https://blog.marcgravell.com/2019/02/fun-with-spiral-of-death.html) problems that this uncovered
-- performance: re-introduce the unsent backlog queue, in particular to improve async performance
-- performance: simplify how completions are reactivated, so that external callers use their originating pool, not the dedicated IO pools (prevent thread stealing)
-- fix: update Pipelines.Sockets.Unofficial to prevent issue with incorrect buffer re-use in corner-case
-- fix: `KeyDeleteAsync` could, in some cases, always use `DEL` (instead of `UNLINK`)
-- fix: last unanswered write time was incorrect
-- change: use higher `Pipe` thresholds when sending
+- Performance: Use new [arena allocation API](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas) to avoid `RawResult[]` overhead
+- Performance: Massively simplified how `ResultBox` is implemented, in particular to reduce `TaskCompletionSource` allocations
+- Performance: Fix sync-over-async issue with async call paths, and fix the [SemaphoreSlim](https://blog.marcgravell.com/2019/02/fun-with-spiral-of-death.html) problems that this uncovered
+- Performance: Reintroduce the unsent backlog queue, in particular to improve async performance
+- Performance: Simplify how completions are reactivated, so that external callers use their originating pool, not the dedicated IO pools (prevent thread stealing)
+- Fix: Update `Pipelines.Sockets.Unofficial` to prevent issue with incorrect buffer re-use in corner-case
+- Fix: `KeyDeleteAsync` could, in some cases, always use `DEL` (instead of `UNLINK`)
+- Fix: Last unanswered write time was incorrect
+- Change: Use higher `Pipe` thresholds when sending
## 2.0.519
-- adapt to late changes in the RC streams API (#983, #1007)
-- documentation fixes (#997, #1005)
-- build: switch to SDK 2.1.500
+- Fix [#1007](https://github.com/StackExchange/StackExchange.Redis/issues/1007): Adapt to late changes in the RC streams API ([#983 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/983))
+- Documentation fixes ([#997 by MerelyRBLX](https://github.com/StackExchange/StackExchange.Redis/pull/997), [#1005 by zBrianW](https://github.com/StackExchange/StackExchange.Redis/pull/1005))
+- Build: Switch to SDK 2.1.500
## 2.0.513
-- fix #961 - fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely)
-- fix #962 - avoid NRE in edge-case when fetching bridge
+- Fix [#961](https://github.com/StackExchange/StackExchange.Redis/issues/962): fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely)
+- Fix [#962](https://github.com/StackExchange/StackExchange.Redis/issues/962): Avoid NRE in edge-case when fetching bridge
## 2.0.505
-- fix #943 - ensure transaction inner tasks are completed prior to completing the outer transaction task
-- fix #946 - reinstate missing `TryParse` methods on `RedisValue`
-- fix #940 - off-by-one on pre-boxed integer cache (NRediSearch)
+- Fix [#943](https://github.com/StackExchange/StackExchange.Redis/issues/943): Ensure transaction inner tasks are completed prior to completing the outer transaction task
+- Fix [#946](https://github.com/StackExchange/StackExchange.Redis/issues/946): Reinstate missing `TryParse` methods on `RedisValue`
+- Fix [#940](https://github.com/StackExchange/StackExchange.Redis/issues/940): Off-by-one on pre-boxed integer cache (NRediSearch)
## 2.0.495
-- 2.0 is a large - and breaking - change
+2.0 is a large - and breaking - change. The key focus of this release is stability and reliability.
-The key focus of this release is stability and reliability.
-
-- HARD BREAK: the package identity has changed; instead of `StackExchange.Redis` (not strong-named) and `StackExchange.Redis.StrongName` (strong-named), we are now
+- **Hard Break**: The package identity has changed; instead of `StackExchange.Redis` (not strong-named) and `StackExchange.Redis.StrongName` (strong-named), we are now
only releasing `StackExchange.Redis` (strong-named). This is a binary breaking change that requires consumers to be re-compiled; it cannot be applied via binding-redirects
-- HARD BREAK: the platform targets have been rationalized - supported targets are .NETStandard 2.0 (and above), .NETFramework 4.6.1 (and above), and .NETFramework 4.7.2 (and above)
+- **Hard Break**: The platform targets have been rationalized - supported targets are .NETStandard 2.0 (and above), .NETFramework 4.6.1 (and above), and .NETFramework 4.7.2 (and above)
(note - the last two are mainly due to assembly binding problems)
-- HARD BREAK: the profiling API has been overhauled and simplified; full documentation is [provided here](https://stackexchange.github.io/StackExchange.Redis/Profiling_v2.html)
-- SOFT BREAK: the `PreserveAsyncOrder` behaviour of the pub/sub API has been deprecated; a *new* API has been provided for scenarios that require in-order pub/sub handling -
- the `Subscribe` method has a new overload *without* a handler parameter which returns a `ChannelMessageQueue`, which provides `async` ordered access to messsages)
-- internal: the network architecture has moved to use `System.IO.Pipelines`; this has allowed us to simplify and unify a lot of the network code, and in particular
- fix a lot of problems relating to how the library worked with TLS and/or .NETStandard
-- change: as a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts.md) has been updated
-- removed: the `HighPriority` (queue-jumping) flag is now deprecated
-- internal: most buffers internally now make use of pooled memory; `RedisValue` no longer pre-emptively allocates buffers
-- internal: added new custom thread-pool for handling async continuations to avoid thread-pool starvation issues
-- internal: all IL generation has been removed; the library should now work on platforms that do not allow runtime-emit
-- added: asynchronous operations now have full support for reporting timeouts
-- added: new APIs now exist to work with pooled memory without allocations - `RedisValue.CreateFrom(MemoryStream)` and `operator` support for `Memory` and `ReadOnlyMemory`; and `IDatabase.StringGetLease[Async](...)`, `IDatabase.HashGetLease[Async](...)`, `Lease.AsStream()`)
-- added: ["streams"](https://redis.io/topics/streams-intro) support (thanks to [ttingen](https://github.com/ttingen) for their contribution)
-- various missing commands / overloads have been added; `Execute[Async]` for additional commands is now available on `IServer`
-- fix: a *lot* of general bugs and issues have been resolved
-- ACCIDENTAL BREAK: `RedisValue.TryParse` was accidentally ommitted in the overhaul; this has been rectified and will be available in the next build
-
-a more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871)
-
-Note: we currently have no plans to do an additional 1.* release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have
-plans to release `1.2.7`.
+- **Hard Break**: The profiling API has been overhauled and simplified; full documentation is [provided here](https://stackexchange.github.io/StackExchange.Redis/Profiling_v2.html)
+- **Soft Break**: The `PreserveAsyncOrder` behaviour of the pub/sub API has been deprecated; a *new* API has been provided for scenarios that require in-order pub/sub handling -
+ the `Subscribe` method has a new overload *without* a handler parameter which returns a `ChannelMessageQueue`, which provides `async` ordered access to messages)
+- Internal: The network architecture has moved to use `System.IO.Pipelines`; this has allowed us to simplify and unify a lot of the network code, and in particular fix a lot of problems relating to how the library worked with TLS and/or .NETStandard
+- Change: As a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts.md) has been updated
+- Removed: The `HighPriority` (queue-jumping) flag is now deprecated
+- Internal: Most buffers internally now make use of pooled memory; `RedisValue` no longer preemptively allocates buffers
+- Internal: Added new custom thread-pool for handling async continuations to avoid thread-pool starvation issues
+- Internal: All IL generation has been removed; the library should now work on platforms that do not allow runtime-emit
+- Adds: asynchronous operations now have full support for reporting timeouts
+- Adds: new APIs now exist to work with pooled memory without allocations - `RedisValue.CreateFrom(MemoryStream)` and `operator` support for `Memory` and `ReadOnlyMemory`; and `IDatabase.StringGetLease[Async](...)`, `IDatabase.HashGetLease[Async](...)`, `Lease.AsStream()`)
+- Adds: ["streams"](https://redis.io/topics/streams-intro) support (thanks to [ttingen](https://github.com/ttingen) for their contribution)
+- Adds: Various missing commands / overloads have been added; `Execute[Async]` for additional commands is now available on `IServer`
+- Fix: A *lot* of general bugs and issues have been resolved
+- **Break**: `RedisValue.TryParse` was accidentally omitted in the overhaul; this has been rectified and will be available in the next build
+
+A more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871)
+
+Note: we currently have no plans to do an additional `1.*` release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have plans to release `1.2.7`.
---
## 1.2.6
-- fix change to `cluster nodes` output when using cluster-enabled target and 4.0+ (see [redis #4186](https://github.com/antirez/redis/issues/4186)
+- Change: `cluster nodes` output when using cluster-enabled target and 4.0+ (see [redis #4186](https://github.com/antirez/redis/issues/4186)
## 1.2.5
-- critical fix: "poll mode" was disabled in the build for net45/net60 - impact: IO jams and lack of reader during high load
+- (Critical) Fix: "poll mode" was disabled in the build for `net45`/`net46` - Impact: IO jams and lack of reader during high load
## 1.2.4
-- fix: incorrect build configuration (#649)
+- Fix: Incorrect build configuration ([#649 by jrlost](https://github.com/StackExchange/StackExchange.Redis/issues/649))
## 1.2.3
-- fix: when using `redis-cluster` with multiple replicas, use round-robin when selecting replica (#610)
-- add: can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) (#617)
+- Fix: When using `redis-cluster` with multiple replicas, use round-robin when selecting replica ([#610 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/610))
+- Adds: Can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) ([#617 by Funbit](https://github.com/StackExchange/StackExchange.Redis/issues/617))
-## 1.2.2 (preview):
+## 1.2.2 (preview)
-- **UNAVAILABLE**: .NET 4.0 support is not in this build, due to [a build issue](https://github.com/dotnet/cli/issues/5993) - looking into solutions
-- add: make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems (#587)
-- add: can now specifiy allowed SSL/TLS protocols (#603)
-- add: track message status in exceptions (#576)
-- add: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided)
-- improved connection retry policy (#510, #572)
-- add `Execute`/`ExecuteAsync` API to support "modules"; [more info](http://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html)
-- fix: timeout link fixed re /docs change (below)
+- **Break**: .NET 4.0 support is not in this build, due to [a build issue](https://github.com/dotnet/cli/issues/5993) - looking into solutions
+- Adds: Make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems ([#587 by AlexanderKot](https://github.com/StackExchange/StackExchange.Redis/issues/587))
+- Adds: Can now specifiy allowed SSL/TLS protocols ([#603 by JonCole](https://github.com/StackExchange/StackExchange.Redis/pull/603))
+- Adds: Track message status in exceptions ([#576 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/576))
+- Adds: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided)
+- Performance: Improved connection retry policy ([#510 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/510), [#572 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/572))
+- Adds: `Execute`/`ExecuteAsync` API to support "modules"; [more info](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html)
+- Fix: Timeout link fixed re /docs change (below)
- [`NRediSearch`](https://www.nuget.org/packages/NRediSearch/) added as exploration into "modules"
-
-Other changes (not library related)
-
-- (project) refactor /docs for github pages
-- improve release note tracking
-- rework build process to use csproj
+- Other changes (not library related)
+ - Change: Refactor /docs for github pages
+ - Change: Improve release note tracking
+ - Build: Rework build process to use csproj
## 1.2.1
-- fix: avoid overlapping per-endpoint heartbeats
-
-## 1.2.0
-
-- (same as 1.2.0-alpha1)
+- Fix: Avoid overlapping per-endpoint heartbeats
-## 1.2.0-alpha1
+## 1.2.0 (same as 1.2.0-alpha1)
-- add: GEO commands (#489)
-- add: ZADD support for new NX/XX switches (#520)
-- add: core-clr preview support improvements
+- Adds: GEO commands ([#489 by wjdavis5](https://github.com/StackExchange/StackExchange.Redis/pull/489))
+- Adds: ZADD support for new NX/XX switches ([#520 by seniorquico](https://github.com/StackExchange/StackExchange.Redis/pull/520))
+- Adds: core-clr preview support improvements
## 1.1.608
-- fix: bug with race condition in servers indexer (related: 1.1.606)
+- Fix: Bug with race condition in servers indexer (related: 1.1.606)
## 1.1.607
-- fix: ensure socket-mode polling is enabled (.net)
+- Fix: Ensure socket-mode polling is enabled (.net)
## 1.1.606
-- fix: bug with race condition in servers indexer
+- Fix: Bug with race condition in servers indexer
-## and the rest
+## ...and the rest
-(I'm happy to take PRs for change history going back in time)
+(We're happy to take PRs for change history going back in time or any fixes here!)
diff --git a/docs/Resp3.md b/docs/Resp3.md
new file mode 100644
index 000000000..126b460f4
--- /dev/null
+++ b/docs/Resp3.md
@@ -0,0 +1,45 @@
+# RESP3 and StackExchange.Redis
+
+RESP2 and RESP3 are evolutions of the Redis protocol, with RESP3 existing from Redis server version 6 onwards (v7.2+ for Redis Enterprise). The main differences are:
+
+1. RESP3 can carry out-of-band / "push" messages on a single connection, where-as RESP2 requires a separate connection for these messages
+2. RESP3 can (when appropriate) convey additional semantic meaning about returned payloads inside the same result structure
+3. Some commands (see [this topic](https://github.com/redis/redis-doc/issues/2511)) return different result structures in RESP3 mode; for example a flat interleaved array might become a jagged array
+
+For most people, #1 is the main reason to consider RESP3, as in high-usage servers - this can halve the number of connections required.
+This is particularly useful in hosted environments where the number of inbound connections to the server is capped as part of a service plan.
+Alternatively, where users are currently choosing to disable the out-of-band connection to achieve this, they may now be able to re-enable this
+(for example, to receive server maintenance notifications) *without* incurring any additional connection overhead.
+
+Because of the significance of #3 (and to avoid breaking your code), the library does not currently default to RESP3 mode. This must be enabled explicitly
+via `ConfigurationOptions.Protocol` or by adding `,protocol=resp3` (or `,protocol=3`) to the configuration string.
+
+---
+
+#3 is a critical one - the library *should* already handle all documented commands that have revised results in RESP3, but if you're using
+`Execute[Async]` to issue ad-hoc commands, you may need to update your processing code to compensate for this, ideally using detection to handle
+*either* format so that the same code works in both REP2 and RESP3. Since the impacted commands are handled internally by the library, in reality
+this should not usually present a difficulty.
+
+The minor (#2) and major (#3) differences to results are only visible to your code when using:
+
+- Lua scripts invoked via the `ScriptEvaluate[Async](...)` or related APIs, that either:
+ - Uses the `redis.setresp(3)` API and returns a value from `redis.[p]call(...)`
+ - Returns a value that satisfies the [LUA to RESP3 type conversion rules](https://redis.io/docs/manual/programmability/lua-api/#lua-to-resp3-type-conversion)
+- Ad-hoc commands (in particular: *modules*) that are invoked via the `Execute[Async](string command, ...)` API
+
+...both which return `RedisResult`. **If you are not using these APIs, you should not need to do anything additional.**
+
+Historically, you could use the `RedisResult.Type` property to query the type of data returned (integer, string, etc). In particular:
+
+- Two new properties are added: `RedisResult.Resp2Type` and `RedisResult.Resp3Type`
+ - The `Resp3Type` property exposes the new semantic data (when using RESP3) - for example, it can indicate that a value is a double-precision number, a boolean, a map, etc (types that did not historically exist)
+ - The `Resp2Type` property exposes the same value that *would* have been returned if this data had been returned over RESP2
+ - The `Type` property is now marked obsolete, but functions identically to `Resp2Type`, so that pre-existing code (for example, that has a `switch` on the type) is not impacted by RESP3
+- The `ResultType.MultiBulk` is superseded by `ResultType.Array` (this is a nomenclature change only; they are the same value and function identically)
+
+Possible changes required due to RESP3:
+
+1. To prevent build warnings, replace usage of `ResultType.MultiBulk` with `ResultType.Array`, and usage of `RedisResult.Type` with `RedisResult.Resp2Type`
+2. If you wish to exploit the additional semantic data when enabling RESP3, use `RedisResult.Resp3Type` where appropriate
+3. If you are enabling RESP3, you must verify whether the commands you are using can give different result shapes on RESP3 connections
\ No newline at end of file
diff --git a/docs/RespLogging.md b/docs/RespLogging.md
new file mode 100644
index 000000000..3ff3b0164
--- /dev/null
+++ b/docs/RespLogging.md
@@ -0,0 +1,150 @@
+Logging and validating the underlying RESP stream
+===
+
+Sometimes (rarely) there is a question over the validity of the RESP stream from a server (especially when using proxies
+or a "redis-like-but-not-actually-redis" server), and it is hard to know whether the *data sent* was bad, vs
+the client library tripped over the data.
+
+To help with this, an experimental API exists to help log and validate RESP streams. This API is not intended
+for routine use (and may change at any time), but can be useful for diagnosing problems.
+
+For example, consider we have the following load test which (on some setup) causes a failure with some
+degree of reliability (even if you need to run it 6 times to see a failure):
+
+``` c#
+// connect
+Console.WriteLine("Connecting...");
+var options = ConfigurationOptions.Parse(ConnectionString);
+await using var muxer = await ConnectionMultiplexer.ConnectAsync(options);
+var db = muxer.GetDatabase();
+
+// load
+RedisKey testKey = "marc_abc";
+await db.KeyDeleteAsync(testKey);
+Console.WriteLine("Writing...");
+for (int i = 0; i < 100; i++)
+{
+ // sync every 50 iterations (pipeline the rest)
+ var flags = (i % 50) == 0 ? CommandFlags.None : CommandFlags.FireAndForget;
+ await db.SetAddAsync(testKey, Guid.NewGuid().ToString(), flags);
+}
+
+// fetch
+Console.WriteLine("Reading...");
+int count = 0;
+for (int i = 0; i < 10; i++)
+{
+ // this is deliberately not using SCARD
+ // (to put load on the inbound)
+ count += (await db.SetMembersAsync(testKey)).Length;
+}
+Console.WriteLine("all done");
+```
+
+## Logging RESP streams
+
+When this fails, it will not be obvious exactly who is to blame. However, we can ask for the data streams
+to be logged to the local file-system.
+
+**Obviously, this may leave data on disk, so this may present security concerns if used with production data; use
+this feature sparingly, and clean up after yourself!**
+
+``` c#
+// connect
+Console.WriteLine("Connecting...");
+var options = ConfigurationOptions.Parse(ConnectionString);
+LoggingTunnel.LogToDirectory(options, @"C:\Code\RedisLog"); // <=== added!
+await using var muxer = await ConnectionMultiplexer.ConnectAsync(options);
+...
+```
+
+This API is marked `[Obsolete]` simply to discourage usage, but you can ignore this warning once you
+understand what it is saying (using `#pragma warning disable CS0618` if necessary).
+
+This will update the `ConfigurationOptions` with a custom `Tunnel` that performs file-based mirroring
+of the RESP streams. If `Ssl` is enabled on the `ConfigurationOptions`, the `Tunnel` will *take over that responsibility*
+(so that the unencrypted data can be logged), and will *disable* `Ssl` on the `ConfigurationOptions` - but TLS
+will still be used correctly.
+
+If we run our code, we will see that 2 files are written per connection ("in" and "out"); if you are using RESP2 (the default),
+then 2 connections are usually established (one for regular "interactive" commands, and one for pub/sub messages), so this will
+typically create 4 files.
+
+## Validating RESP streams
+
+RESP is *mostly* text, so a quick eyeball can be achieved using any text tool; an "out" file will typically start:
+
+``` txt
+$6
+CLIENT
+$7
+SETNAME
+...
+```
+
+and an "in" file will typically start:
+
+``` txt
++OK
++OK
++OK
+...
+```
+
+This is the start of the handshakes for identifying the client to the redis server, and the server acknowledging this (if
+you have authentication enabled, there will be a `AUTH` command first, or `HELLO` on RESP3).
+
+If there is a failure, you obviously don't want to manually check these files. Instead, an API exists to validate RESP streams:
+
+``` c#
+var messages = await LoggingTunnel.ValidateAsync(@"C:\Code\RedisLog");
+Console.WriteLine($"{messages} RESP fragments validated");
+```
+
+If the RESP streams are *not* valid, an exception will provide further details.
+
+**An exception here is strong evidence that there is a fault either in the redis server, or an intermediate proxy**.
+
+Conversely, if the library reported a protocol failure but the validation step here *does not* report an error, then
+that is strong evidence of a library error; [**please report this**](https://github.com/StackExchange/StackExchange.Redis/issues/new) (with details).
+
+You can also *replay* the conversation locally, seeing the individual requests and responses:
+
+``` c#
+var messages = await LoggingTunnel.ReplayAsync(@"C:\Code\RedisLog", (cmd, resp) =>
+{
+ if (cmd.IsNull)
+ {
+ // out-of-band/"push" response
+ Console.WriteLine("<< " + LoggingTunnel.DefaultFormatResponse(resp));
+ }
+ else
+ {
+ Console.WriteLine(" > " + LoggingTunnel.DefaultFormatCommand(cmd));
+ Console.WriteLine(" < " + LoggingTunnel.DefaultFormatResponse(resp));
+ }
+});
+Console.WriteLine($"{messages} RESP commands validated");
+```
+
+The `DefaultFormatCommand` and `DefaultFormatResponse` methods are provided for convenience, but you
+can perform your own formatting logic if required. If a RESP erorr is encountered in the response to
+a particular message, the callback will still be invoked to indicate that error. For example, after deliberately
+introducing an error into the captured file, we might see:
+
+``` txt
+ > CLUSTER NODES
+ < -ERR This instance has cluster support disabled
+ > GET __Booksleeve_TieBreak
+ < (null)
+ > ECHO ...
+ < -Invalid bulk string terminator
+Unhandled exception. StackExchange.Redis.RedisConnectionException: Invalid bulk string terminator
+```
+
+The `-ERR` message is not a problem - that's normal and simply indicates that this is not a redis cluster; however, the
+final pair is an `ECHO` request, for which the corresponding response was invalid. This information is useful for finding
+out what happened.
+
+Emphasis: this API is not intended for common/frequent usage; it is intended only to assist validating the underlying
+RESP stream.
\ No newline at end of file
diff --git a/docs/Scripting.md b/docs/Scripting.md
index e04b2d19a..56af7afc1 100644
--- a/docs/Scripting.md
+++ b/docs/Scripting.md
@@ -1,24 +1,23 @@
Scripting
===
-Basic [Lua scripting](http://redis.io/commands/EVAL) is supported by the `IServer.ScriptLoad(Async)`, `IServer.ScriptExists(Async)`, `IServer.ScriptFlush(Async)`, `IDatabase.ScriptEvaluate`, and `IDatabaseAsync.ScriptEvaluateAsync` methods.
+Basic [Lua scripting](https://redis.io/commands/EVAL) is supported by the `IServer.ScriptLoad(Async)`, `IServer.ScriptExists(Async)`, `IServer.ScriptFlush(Async)`, `IDatabase.ScriptEvaluate`, and `IDatabaseAsync.ScriptEvaluateAsync` methods.
These methods expose the basic commands necessary to submit and execute Lua scripts to redis.
-More sophisticated scripting is available through the `LuaScript` class. The `LuaScript` class makes it simpler to prepare and submit parameters along with a script, as well as allowing you to use
-cleaner variables names.
+More sophisticated scripting is available through the `LuaScript` class. The `LuaScript` class makes it simpler to prepare and submit parameters along with a script, as well as allowing you to use cleaner variables names.
An example use of the `LuaScript`:
-```
- const string Script = "redis.call('set', @key, @value)";
+```csharp
+const string Script = "redis.call('set', @key, @value)";
- using (ConnectionMultiplexer conn = /* init code */)
- {
- var db = conn.GetDatabase(0);
+using (ConnectionMultiplexer conn = /* init code */)
+{
+ var db = conn.GetDatabase(0);
- var prepared = LuaScript.Prepare(Script);
- db.ScriptEvaluate(prepared, new { key = (RedisKey)"mykey", value = 123 });
- }
+ var prepared = LuaScript.Prepare(Script);
+ db.ScriptEvaluate(prepared, new { key = (RedisKey)"mykey", value = 123 });
+}
```
The `LuaScript` class rewrites variables in scripts of the form `@myVar` into the appropriate `ARGV[someIndex]` required by redis. If the
@@ -36,24 +35,25 @@ Any object that exposes field or property members with the same name as @-prefix
- RedisKey
- RedisValue
+StackExchange.Redis handles Lua script caching internally. It automatically transmits the Lua script to redis on the first call to 'ScriptEvaluate'. For further calls of the same script only the hash with [`EVALSHA`](https://redis.io/commands/evalsha) is used.
-To avoid retransmitting the Lua script to redis each time it is evaluated, `LuaScript` objects can be converted into `LoadedLuaScript`s via `LuaScript.Load(IServer)`.
-`LoadedLuaScripts` are evaluated with the [`EVALSHA`](http://redis.io/commands/evalsha), and referred to by hash.
+For more control of the Lua script transmission to redis, `LuaScript` objects can be converted into `LoadedLuaScript`s via `LuaScript.Load(IServer)`.
+`LoadedLuaScripts` are evaluated with the [`EVALSHA`](https://redis.io/commands/evalsha), and referred to by hash.
An example use of `LoadedLuaScript`:
-```
- const string Script = "redis.call('set', @key, @value)";
+```csharp
+const string Script = "redis.call('set', @key, @value)";
- using (ConnectionMultiplexer conn = /* init code */)
- {
- var db = conn.GetDatabase(0);
- var server = conn.GetServer(/* appropriate parameters*/);
+using (ConnectionMultiplexer conn = /* init code */)
+{
+ var db = conn.GetDatabase(0);
+ var server = conn.GetServer(/* appropriate parameters*/);
- var prepared = LuaScript.Prepare(Script);
- var loaded = prepared.Load(server);
- loaded.Evaluate(db, new { key = (RedisKey)"mykey", value = 123 });
- }
+ var prepared = LuaScript.Prepare(Script);
+ var loaded = prepared.Load(server);
+ loaded.Evaluate(db, new { key = (RedisKey)"mykey", value = 123 });
+}
```
All methods on both `LuaScript` and `LoadedLuaScript` have Async alternatives, and expose the actual script submitted to redis as the `ExecutableScript` property.
diff --git a/docs/Server.md b/docs/Server.md
index e236b1f5f..a0777c478 100644
--- a/docs/Server.md
+++ b/docs/Server.md
@@ -13,7 +13,7 @@ There are multiple ways of running redis on windows:
- [Memurai](https://www.memurai.com/) : a fully supported, well-maintained port of redis for Windows (this is a commercial product, with a free developer version available, and free trials)
- previous to Memurai, MSOpenTech had a Windows port of linux, but this is no longer maintained and is now very out of date; it is not recommended, but: [here](https://www.nuget.org/packages/redis-64/)
-- WSL/WSL2 : on Windows 10, you can run redis for linux in the Windows Subsystem for Linux; note, however, that WSL may have some significant performance implications, and WSL2 appears as a *different* machine (not the local machine), due to running as a VM
+- WSL/WSL2 : on Windows 10+, you can run redis for linux in the Windows Subsystem for Linux; note, however, that WSL may have some significant performance implications, and WSL2 appears as a *different* machine (not the local machine), due to running as a VM
## Docker
@@ -25,4 +25,5 @@ If you don't want to run your own redis servers, multiple commercial cloud offer
- RedisLabs
- Azure Redis Cache
-- AWS ElastiCache for Redis
\ No newline at end of file
+- AWS ElastiCache for Redis
+- GCP Memorystore for Redis
diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md
new file mode 100644
index 000000000..2f4ba1c29
--- /dev/null
+++ b/docs/ServerMaintenanceEvent.md
@@ -0,0 +1,67 @@
+# Introducing ServerMaintenanceEvents
+
+StackExchange.Redis now automatically subscribes to notifications about upcoming maintenance from supported Redis providers. The ServerMaintenanceEvent on the ConnectionMultiplexer raises events in response to notifications about server maintenance, and application code can subscribe to the event to handle connection drops more gracefully during these maintenance operations.
+
+If you are a Redis vendor and want to integrate support for ServerMaintenanceEvents into StackExchange.Redis, we recommend opening an issue so we can discuss the details.
+
+## Types of events
+
+Azure Cache for Redis currently sends the following notifications:
+* `NodeMaintenanceScheduled`: Indicates that a maintenance event is scheduled. Can be 10-15 minutes in advance.
+* `NodeMaintenanceStarting`: This event gets fired ~20s before maintenance begins
+* `NodeMaintenanceStart`: This event gets fired when maintenance is imminent (<5s)
+* `NodeMaintenanceFailoverComplete`: Indicates that a replica has been promoted to primary
+* `NodeMaintenanceEnded`: Indicates that the node maintenance operation is over
+
+## Sample code
+
+The library will automatically subscribe to the pub/sub channel to receive notifications from the server, if one exists. For Azure Redis caches, this is the 'AzureRedisEvents' channel. To plug in your maintenance handling logic, you can pass in an event handler via the `ServerMaintenanceEvent` event on your `ConnectionMultiplexer`. For example:
+
+```csharp
+multiplexer.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) =>
+{
+ if (e is AzureMaintenanceEvent azureEvent && azureEvent.NotificationType == AzureNotificationType.NodeMaintenanceStart)
+ {
+ // Take whatever action is appropriate for your application to handle the maintenance operation gracefully.
+ // This might mean writing a log entry, redirecting traffic away from the impacted Redis server, or
+ // something entirely different.
+ }
+};
+```
+You can see the schema for the `AzureMaintenanceEvent` class [here](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs). Note that the library automatically sets the `ReceivedTimeUtc` timestamp when the event is received, so if you see in your logs that `ReceivedTimeUtc` is after `StartTimeUtc`, this may indicate that your connections are under high load.
+
+## Walking through a sample maintenance event
+
+1. App is connected to Redis and everything is working fine.
+2. Current Time: [16:21:39] -> `NodeMaintenanceScheduled` event is raised, with a `StartTimeUtc` of 16:35:57 (about 14 minutes from current time).
+ * Note: the start time for this event is an approximation, because we will start getting ready for the update proactively and the node may become unavailable up to 3 minutes sooner. We recommend listening for `NodeMaintenanceStarting` and `NodeMaintenanceStart` for the highest level of accuracy (these are only likely to differ by a few seconds at most).
+3. Current Time: [16:34:26] -> `NodeMaintenanceStarting` message is received, and `StartTimeUtc` is 16:34:46, about 20 seconds from the current time.
+4. Current Time: [16:34:46] -> `NodeMaintenanceStart` message is received, so we know the node maintenance is about to happen. We break the circuit and stop sending new operations to the Redis connection. (Note: the appropriate action for your application may be different.) StackExchange.Redis will automatically refresh its view of the overall server topology.
+5. Current Time: [16:34:47] -> The connection is closed by the Redis server.
+6. Current Time: [16:34:56] -> `NodeMaintenanceFailoverComplete` message is received. This tells us that the replica node has promoted itself to primary, so the other node can go offline for maintenance.
+7. Current Time [16:34:56] -> The connection to the Redis server is restored. It is safe to send commands again to the connection and all commands will succeed.
+8. Current Time [16:37:48] -> `NodeMaintenanceEnded` message is received, with a `StartTimeUtc` of 16:37:48. Nothing to do here if you are talking to the load balancer endpoint (port 6380 or 6379). For clustered servers, you can resume sending readonly workloads to the replica(s).
+
+## Azure Cache for Redis Maintenance Event details
+
+#### NodeMaintenanceScheduled event
+
+`NodeMaintenanceScheduled` events are raised for maintenance scheduled by Azure, up to 15 minutes in advance. This event will not get fired for user-initiated reboots.
+
+#### NodeMaintenanceStarting event
+
+`NodeMaintenanceStarting` events are raised ~20 seconds ahead of upcoming maintenance. This means that one of the primary or replica nodes will be going down for maintenance.
+
+It's important to understand that this does *not* mean downtime if you are using a Standard/Premier SKU cache. If the replica is targeted for maintenance, disruptions should be minimal. If the primary node is the one going down for maintenance, a failover will occur, which will close existing connections going through the load balancer port (6380/6379) or directly to the node (15000/15001). You may want to pause sending write commands until the replica node has assumed the primary role and the failover is complete.
+
+#### NodeMaintenanceStart event
+
+`NodeMaintenanceStart` events are raised when maintenance is imminent (within seconds). These messages do not include a `StartTimeUtc` because they are fired immediately before maintenance occurs.
+
+#### NodeMaintenanceFailoverComplete event
+
+`NodeMaintenanceFailoverComplete` events are raised when a replica has promoted itself to primary. These events do not include a `StartTimeUtc` because the action has already occurred.
+
+#### NodeMaintenanceEnded event
+
+`NodeMaintenanceEnded` events are raised to indicate that the maintenance operation has completed and that the replica is once again available. You do *NOT* need to wait for this event to use the load balancer endpoint, as it is available throughout. However, we included this for logging purposes and for customers who use the replica endpoint in clusters for read workloads.
\ No newline at end of file
diff --git a/docs/Streams.md b/docs/Streams.md
index c7f278d17..47e82c2b9 100644
--- a/docs/Streams.md
+++ b/docs/Streams.md
@@ -12,7 +12,7 @@ Use the following to add a simple message with a single name/value pair to a str
```csharp
var db = redis.GetDatabase();
-var messageId = db.StreamAdd("event_stream", "foo_name", "bar_value");
+var messageId = db.StreamAdd("events_stream", "foo_name", "bar_value");
// messageId = 1518951480106-0
```
@@ -34,16 +34,34 @@ var messageId = db.StreamAdd("sensor_stream", values);
You also have the option to override the auto-generated message ID by passing your own ID to the `StreamAdd` method. Other optional parameters allow you to trim the stream's length.
```csharp
-db.StreamAdd("event_stream", "foo_name", "bar_value", messageId: "0-1", maxLength: 100);
+db.StreamAdd("events_stream", "foo_name", "bar_value", messageId: "0-1", maxLength: 100);
```
+Idempotent write-at-most-once production
+===
+
+From Redis 8.6, streams support idempotent write-at-most-once production. This is achieved by passing a `StreamIdempotentId` to the `StreamAdd` method. Using idempotent ids avoids
+duplicate entries in the stream, even in the event of a failure and retry.
+
+The `StreamIdempotentId` contains a producer id and an optional idempotent id. The producer id should be unique for a given data generator and should be stable and consistent between runs.
+The optional idempotent id should be unique for a given data item. If the idempotent id is not provided, the server will generate it from the content of the data item.
+
+```csharp
+// int someUniqueExternalSourceId = ... // optional
+var idempotentId = new StreamIdempotentId("ticket_generator");
+// optionally, new StreamIdempotentId("ticket_generator", someUniqueExternalSourceId)
+var messageId = db.StreamAdd("events_stream", "foo_name", "bar_value", idempotentId);
+```
+
+~~~~The `StreamConfigure` method can be used to configure the stream, in particular the IDMP map. The `StreamConfiguration` class has properties for the idempotent producer (IDMP) duration and max-size.
+
Reading from Streams
===
Reading from a stream is done by using either the `StreamRead` or `StreamRange` methods.
```csharp
-var messages = db.StreamRead("event_stream", "0-0");
+var messages = db.StreamRead("events_stream", "0-0");
```
The code above will read all messages from the ID `"0-0"` to the end of the stream. You have the option to limit the number of messages returned by using the optional `count` parameter.
@@ -53,7 +71,7 @@ The `StreamRead` method also allows you to read from multiple streams at once:
```csharp
var streams = db.StreamRead(new StreamPosition[]
{
- new StreamPosition("event_stream", "0-0"),
+ new StreamPosition("events_stream", "0-0"),
new StreamPosition("score_stream", "0-0")
});
@@ -66,13 +84,13 @@ You can limit the number of messages returned per stream by using the `countPerS
The `StreamRange` method allows you to return a range of entries within a stream.
```csharp
-var messages = db.StreamRange("event_stream", minId: "-", maxId: "+");
+var messages = db.StreamRange("events_stream", minId: "-", maxId: "+");
```
The `"-"` and `"+"` special characters indicate the smallest and greatest IDs possible. These values are the default values that will be used if no value is passed for the respective parameter. You also have the option to read the stream in reverse by using the `messageOrder` parameter. The `StreamRange` method also provides the ability to limit the number of entries returned by using the `count` parameter.
```csharp
-var messages = db.StreamRange("event_stream",
+var messages = db.StreamRange("events_stream",
minId: "0-0",
maxId: "+",
count: 100,
@@ -85,7 +103,7 @@ Stream Information
The `StreamInfo` method provides the ability to read basic information about a stream: its first and last entry, the stream's length, the number of consumer groups, etc. This information can be used to process a stream in a more efficient manner.
```csharp
-var info = db.StreamInfo("event_stream");
+var info = db.StreamInfo("events_stream");
Console.WriteLine(info.Length);
Console.WriteLine(info.FirstEntry.Id);
diff --git a/docs/Testing.md b/docs/Testing.md
index f9c812811..52776f3b6 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -4,24 +4,24 @@ Testing
Welcome to documentation for the `StackExchange.Redis` test suite!
Supported platforms:
-- Windows
-
-...that's it. For now. I'll add Docker files for the instances soon, unless someone's willing to get to it first. The tests (for `netcoreapp`) can run multi-platform.
-
-**Note: some tests are not yet green, about 20 are failing (~31 in CI)**. A large set of .NET Core, testing, and CI changes just slammed us, we're getting back in action.
+- Windows (all tests)
+- Other .NET-supported platforms (.NET Core tests)
The unit and integration tests here are fairly straightforward. There are 2 primary steps:
1. Start the servers
+
+This can be done either by installing Docker and running `docker compose up` in the `tests\RedisConfigs` folder or by running the `start-all` script in the same folder. Docker is the preferred method.
+
2. Run the tests
-Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/Hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc.
+Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc.
-You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/master/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this:
+You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/main/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this:
```json
{
"RunLongRunning": true,
- "MasterServer": "192.168.0.42",
- "MasterPort": 12345
+ "PrimaryServer": "192.168.0.42",
+ "PrimaryPort": 12345
}
```
Note: if a server isn't specified, the related tests should be skipped as inconclusive.
@@ -30,12 +30,4 @@ You can find all the JSON properties at [TestConfig.cs](https://github.com/Stack
The tests are run (by default) as part of the build. You can simply run this in the repository root:
```cmd
.\build.cmd -BuildNumber local
-```
-
-To specifically run the tests with far more options, from the repository root:
-```cmd
-dotnet build
-.\RedisConfigs\start-all.cmd
-cd StackExchange.Redis.Tests
-dotnet xunit
-```
+```
\ No newline at end of file
diff --git a/docs/ThreadTheft.md b/docs/ThreadTheft.md
index 339fac554..d5b8e717e 100644
--- a/docs/ThreadTheft.md
+++ b/docs/ThreadTheft.md
@@ -3,7 +3,7 @@
If you're here because you followed a link in an exception and you just want your code to work,
the short version is: try adding the following *early on* in your application startup:
-``` c#
+```csharp
ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true);
```
@@ -40,14 +40,13 @@ be an asynchronous dispatch API. But... not all implementations are equal. Some
in particular of `LegacyAspNetSynchronizationContext`, which is what you get if you
configure ASP.NET with:
-
``` xml
```
-or
+or if you do _not_ have a `` of at least 4.5 (which causes the above to default `true`) like this:
-```
+```xml
```
diff --git a/docs/Timeouts.md b/docs/Timeouts.md
index d608b06e1..ea9830041 100644
--- a/docs/Timeouts.md
+++ b/docs/Timeouts.md
@@ -10,7 +10,7 @@ it is possible that the reader loop has been hijacked; see [Thread Theft](Thread
Are there commands taking a long time to process on the redis-server?
---------------
-There can be commands that are taking a long time to process on the redis-server causing the request to timeout. Few examples of long running commands are mget with large number of keys, keys * or poorly written lua script. You can run the SlowLog command to see if there are requests taking longer than expected. More details regarding the command can be found [here](https://redis.io/commands/slowlog).
+There can be commands that are taking a long time to process on the redis-server causing the request to timeout. Few examples of long running commands are mget with large number of keys, keys * or poorly written lua script. You can run [the `SLOWLOG` command](https://redis.io/commands/slowlog) to see if there are requests taking longer than expected. More details regarding the command can be found [here](https://redis.io/commands/slowlog).
Was there a big request preceding several small requests to the Redis that timed out?
---------------
@@ -71,13 +71,15 @@ How to configure this setting:
> **Important Note:** the value specified in this configuration element is a *per-core* setting. For example, if you have a 4 core machine and want your minIOThreads setting to be 200 at runtime, you would use ``.
- - Outside of ASP.NET, use the [ThreadPool.SetMinThreads(…)](https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=netcore-2.0#System_Threading_ThreadPool_SetMinThreads_System_Int32_System_Int32_) API.
-
-- In .Net Core, add Environment Variable COMPlus_ThreadPool_ForceMinWorkerThreads to overwrite default MinThreads setting, according to [Environment/Registry Configuration Knobs](https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/clr-configuration-knobs.md) - You can also use the same ThreadPool.SetMinThreads() Method as described above.
+ - Outside of ASP.NET, use one of the methods described in [Run-time configuration options for threading
+](https://docs.microsoft.com/dotnet/core/run-time-config/threading#minimum-threads):
+ - [ThreadPool.SetMinThreads(…)](https://docs.microsoft.com/dotnet/api/system.threading.threadpool.setminthreads)
+ - The `ThreadPoolMinThreads` MSBuild property
+ - The `System.Threading.ThreadPool.MinThreads` setting in your `runtimeconfig.json`
Explanation for abbreviations appearing in exception messages
---
-By default Redis Timeout exception(s) includes useful information, which can help in uderstanding & diagnosing the timeouts. Some of the abbrivations are as follows:
+By default Redis Timeout exception(s) includes useful information, which can help in understanding & diagnosing the timeouts. Some of the abbreviations are as follows:
| Abbreviation | Long Name | Meaning |
| ------------- | ---------------------- | ---------------------------- |
@@ -85,8 +87,8 @@ By default Redis Timeout exception(s) includes useful information, which can hel
|qu | Queue-Awaiting-Write : {int}|There are x operations currently waiting in queue to write to the redis server.|
|qs | Queue-Awaiting-Response : {int}|There are x operations currently awaiting replies from redis server.|
|aw | Active-Writer: {bool}||
-|bw | Backlog-Writer: {enum} | Possible values are Inactive, Started, CheckingForWork, CheckingForTimeout, RecordingTimeout, WritingMessage, Flushing, MarkingInactive, RecordingWriteFailure, RecordingFault,SettingIdle,Faulted|
-|rs | Read-State: {enum}|Possible values are NotStarted, Init, RanToCompletion, Faulted, ReadSync, ReadAsync, UpdateWriteTime, ProcessBuffer, MarkProcessed, TryParseResult, MatchResult, PubSubMessage, PubSubPMessage, Reconfigure, InvokePubSub, DequeueResult, ComputeResult, CompletePendingMessage, NA|
+|bw | Backlog-Writer: {enum} | Possible values are Inactive, Started, CheckingForWork, CheckingForTimeout, RecordingTimeout, WritingMessage, Flushing, MarkingInactive, RecordingWriteFailure, RecordingFault, SettingIdle, SpinningDown, Faulted|
+|rs | Read-State: {enum}|Possible values are NotStarted, Init, RanToCompletion, Faulted, ReadSync, ReadAsync, UpdateWriteTime, ProcessBuffer, MarkProcessed, TryParseResult, MatchResult, PubSubMessage, PubSubSMessage, PubSubPMessage, Reconfigure, InvokePubSub, DequeueResult, ComputeResult, CompletePendingMessage, NA|
|ws | Write-State: {enum}| Possible values are Initializing, Idle, Writing, Flushing, Flushed, NA|
|in | Inbound-Bytes : {long}|there are x bytes waiting to be read from the input stream from redis|
|in-pipe | Inbound-Pipe-Bytes: {long}|Bytes waiting to be read|
@@ -94,7 +96,8 @@ By default Redis Timeout exception(s) includes useful information, which can hel
|mgr | 8 of 10 available|Redis Internal Dedicated Thread Pool State|
|IOCP | IOCP: (Busy=0,Free=500,Min=248,Max=500)| Runtime Global Thread Pool IO Threads. |
|WORKER | WORKER: (Busy=170,Free=330,Min=248,Max=500)| Runtime Global Thread Pool Worker Threads.|
-|v | Redis Version: version |Current redis version you are currently using in your application.|
+|POOL | POOL: (Threads=8,QueuedItems=0,CompletedItems=42,Timers=10)| Thread Pool Work Item Stats.|
+|v | Redis Version: version |The `StackExchange.Redis` version you are currently using in your application.|
|active | Message-Current: {string} |Included in exception message when `IncludeDetailInExceptions=True` on multiplexer|
|next | Message-Next: {string} |When `IncludeDetailInExceptions=True` on multiplexer, it might include command and key, otherwise only command.|
|Local-CPU | %CPU or Not Available |When `IncludePerformanceCountersInExceptions=True` on multiplexer, Local CPU %age will be included in exception message. It might not work in all environments where application is hosted. |
diff --git a/docs/Transactions.md b/docs/Transactions.md
index 8752b26ea..4d8deca27 100644
--- a/docs/Transactions.md
+++ b/docs/Transactions.md
@@ -1,7 +1,7 @@
Transactions in Redis
=====================
-Transactions in Redis are not like transactions in, say a SQL database. The [full documentation is here](http://redis.io/topics/transactions),
+Transactions in Redis are not like transactions in, say a SQL database. The [full documentation is here](https://redis.io/topics/transactions),
but to paraphrase:
A transaction in redis consists of a block of commands placed between `MULTI` and `EXEC` (or `DISCARD` for rollback). Once a `MULTI`
@@ -41,14 +41,14 @@ you *can* do is: `WATCH` a key, check data from that key in the normal way, then
If, when you check the data, you discover that you don't actually need the transaction, you can use `UNWATCH` to
forget all the watched keys. Note that watched keys are also reset during `EXEC` and `DISCARD`. So *at the Redis layer*, this is conceptually:
-```
+```lua
WATCH {custKey}
HEXISTS {custKey} "UniqueId"
-(check the reply, then either:)
+-- (check the reply, then either:)
MULTI
HSET {custKey} "UniqueId" {newId}
EXEC
-(or, if we find there was already an unique-id:)
+-- (or, if we find there was already an unique-id:)
UNWATCH
```
@@ -98,13 +98,13 @@ bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);
Lua
---
-You should also keep in mind that Redis 2.6 and above [support Lua scripting](http://redis.io/commands/EVAL), a versatile tool for performing multiple operations as a single atomic unit at the server.
+You should also keep in mind that Redis 2.6 and above [support Lua scripting](https://redis.io/commands/EVAL), a versatile tool for performing multiple operations as a single atomic unit at the server.
Since no other connections are serviced during a Lua script it behaves much like a transaction, but without the complexity of `MULTI` / `EXEC` etc. This also avoids issues such as bandwidth and latency
between the caller and the server, but the trade-off is that it monopolises the server for the duration of the script.
At the Redis layer (and assuming `HSETNX` did not exist) this could be implemented as:
-```
+```lua
EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId}
```
diff --git a/docs/VectorSets.md b/docs/VectorSets.md
new file mode 100644
index 000000000..9a362ec15
--- /dev/null
+++ b/docs/VectorSets.md
@@ -0,0 +1,394 @@
+# Redis Vector Sets
+
+Redis Vector Sets provide efficient storage and similarity search for vector data. SE.Redis provides a strongly-typed API for working with vector sets.
+
+## Prerequisites
+
+### Redis Version
+
+Vector Sets require Redis 8.0 or later.
+
+## Quick Start
+
+Note that the vectors used in these examples are small for illustrative purposes. In practice, you would commonly use much
+larger vectors. The API is designed to efficiently handle large vectors - in particular, the use of `ReadOnlyMemory`
+rather than arrays allows you to work with vectors in "pooled" memory buffers (such as `ArrayPool`), which can be more
+efficient than creating arrays - or even working with raw memory for example memory-mapped-files.
+
+### Adding Vectors
+
+Add vectors to a vector set using `VectorSetAddAsync`:
+
+```csharp
+var db = conn.GetDatabase();
+var key = "product-embeddings";
+
+// Create a vector (e.g., from an ML model)
+var vector = new[] { 0.1f, 0.2f, 0.3f, 0.4f };
+
+// Add a member with its vector
+var request = VectorSetAddRequest.Member("product-123", vector.AsMemory());
+bool added = await db.VectorSetAddAsync(key, request);
+```
+
+### Adding Vectors with Attributes
+
+You can attach JSON metadata to vectors for filtering:
+
+```csharp
+var vector = new[] { 0.1f, 0.2f, 0.3f, 0.4f };
+var request = VectorSetAddRequest.Member(
+ "product-123",
+ vector.AsMemory(),
+ attributesJson: """{"category":"electronics","price":299.99}"""
+);
+await db.VectorSetAddAsync(key, request);
+```
+
+### Similarity Search
+
+Find similar vectors using `VectorSetSimilaritySearchAsync`:
+
+```csharp
+// Search by an existing member
+var query = VectorSetSimilaritySearchRequest.ByMember("product-123");
+query.Count = 10;
+query.WithScores = true;
+
+using var results = await db.VectorSetSimilaritySearchAsync(key, query);
+if (results is not null)
+{
+ foreach (var result in results.Value.Results)
+ {
+ Console.WriteLine($"Member: {result.Member}, Score: {result.Score}");
+ }
+}
+```
+
+Or search by a vector directly:
+
+```csharp
+var queryVector = new[] { 0.15f, 0.25f, 0.35f, 0.45f };
+var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory());
+query.Count = 10;
+query.WithScores = true;
+
+using var results = await db.VectorSetSimilaritySearchAsync(key, query);
+```
+
+### Filtered Search
+
+Use JSON path expressions to filter results:
+
+```csharp
+var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory());
+query.Count = 10;
+query.FilterExpression = "$.category == 'electronics' && $.price < 500";
+query.WithAttributes = true; // Include attributes in results
+
+using var results = await db.VectorSetSimilaritySearchAsync(key, query);
+```
+
+See [Redis filtered search documentation](https://redis.io/docs/latest/develop/data-types/vector-sets/filtered-search/) for filter syntax.
+
+## Vector Set Operations
+
+### Getting Vector Set Information
+
+```csharp
+var info = await db.VectorSetInfoAsync(key);
+if (info is not null)
+{
+ Console.WriteLine($"Dimension: {info.Value.Dimension}");
+ Console.WriteLine($"Length: {info.Value.Length}");
+ Console.WriteLine($"Quantization: {info.Value.Quantization}");
+}
+```
+
+### Checking Membership
+
+```csharp
+bool exists = await db.VectorSetContainsAsync(key, "product-123");
+```
+
+### Removing Members
+
+```csharp
+bool removed = await db.VectorSetRemoveAsync(key, "product-123");
+```
+
+### Getting Random Members
+
+```csharp
+// Get a single random member
+var member = await db.VectorSetRandomMemberAsync(key);
+
+// Get multiple random members
+var members = await db.VectorSetRandomMembersAsync(key, count: 5);
+```
+
+## Range Queries
+
+### Getting Members by Lexicographical Range
+
+Retrieve members in lexicographical order:
+
+```csharp
+// Get all members
+using var allMembers = await db.VectorSetRangeAsync(key);
+// ... access allMembers.Span, etc
+
+// Get members in a specific range
+using var rangeMembers = await db.VectorSetRangeAsync(
+ key,
+ start: "product-100",
+ end: "product-200",
+ count: 50
+);
+// ... access rangeMembers.Span, etc
+
+// Exclude boundaries
+using var members = await db.VectorSetRangeAsync(
+ key,
+ start: "product-100",
+ end: "product-200",
+ exclude: Exclude.Both
+);
+// ... access members.Span, etc
+```
+
+### Enumerating Large Result Sets
+
+For large vector sets, use enumeration to process results in batches:
+
+```csharp
+await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 100))
+{
+ Console.WriteLine($"Processing: {member}");
+}
+```
+
+The enumeration of results is done in batches, so that the client does not need to buffer the entire result set in memory;
+if you exit the loop early, the client and server will stop processing and sending results. This also supports async cancellation:
+
+```csharp
+using var cts = new CancellationTokenSource(); // cancellation not shown
+
+await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 100)
+ .WithCancellation(cts.Token))
+{
+ // ...
+}
+```
+
+## Advanced Configuration
+
+### Quantization
+
+Control vector compression:
+
+```csharp
+var request = VectorSetAddRequest.Member("product-123", vector.AsMemory());
+request.Quantization = VectorSetQuantization.Int8; // Default
+// or VectorSetQuantization.None
+// or VectorSetQuantization.Binary
+await db.VectorSetAddAsync(key, request);
+```
+
+### Dimension Reduction
+
+Use projection to reduce vector dimensions:
+
+```csharp
+var request = VectorSetAddRequest.Member("product-123", vector.AsMemory());
+request.ReducedDimensions = 128; // Reduce from original dimension
+await db.VectorSetAddAsync(key, request);
+```
+
+### HNSW Parameters
+
+Fine-tune the HNSW index:
+
+```csharp
+var request = VectorSetAddRequest.Member("product-123", vector.AsMemory());
+request.MaxConnections = 32; // M parameter (default: 16)
+request.BuildExplorationFactor = 400; // EF parameter (default: 200)
+await db.VectorSetAddAsync(key, request);
+```
+
+### Search Parameters
+
+Control search behavior:
+
+```csharp
+var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory());
+query.SearchExplorationFactor = 500; // Higher = more accurate, slower
+query.Epsilon = 0.1; // Only return similarity >= 0.9
+query.UseExactSearch = true; // Use linear scan instead of HNSW
+await db.VectorSetSimilaritySearchAsync(key, query);
+```
+
+## Working with Vector Data
+
+### Retrieving Vectors
+
+Get the approximate vector for a member:
+
+```csharp
+using var vectorLease = await db.VectorSetGetApproximateVectorAsync(key, "product-123");
+if (vectorLease != null)
+{
+ ReadOnlySpan vector = vectorLease.Value.Span;
+ // Use the vector data
+}
+```
+
+### Managing Attributes
+
+Get and set JSON attributes:
+
+```csharp
+// Get attributes
+var json = await db.VectorSetGetAttributesJsonAsync(key, "product-123");
+
+// Set attributes
+await db.VectorSetSetAttributesJsonAsync(
+ key,
+ "product-123",
+ """{"category":"electronics","updated":"2024-01-15"}"""
+);
+```
+
+### Graph Links
+
+Inspect HNSW graph connections:
+
+```csharp
+// Get linked members
+using var links = await db.VectorSetGetLinksAsync(key, "product-123");
+if (links != null)
+{
+ foreach (var link in links.Value.Span)
+ {
+ Console.WriteLine($"Linked to: {link}");
+ }
+}
+
+// Get links with similarity scores
+using var linksWithScores = await db.VectorSetGetLinksWithScoresAsync(key, "product-123");
+if (linksWithScores != null)
+{
+ foreach (var link in linksWithScores.Value.Span)
+ {
+ Console.WriteLine($"Linked to: {link.Member}, Score: {link.Score}");
+ }
+}
+```
+
+## Memory Management
+
+Vector operations return `Lease` for efficient memory pooling. Always dispose leases:
+
+```csharp
+// Using statement (recommended)
+using var results = await db.VectorSetSimilaritySearchAsync(key, query);
+
+// Or explicit disposal
+var results = await db.VectorSetSimilaritySearchAsync(key, query);
+try
+{
+ // Use results
+}
+finally
+{
+ results?.Dispose();
+}
+```
+
+## Performance Considerations
+
+### Batch Operations
+
+For bulk inserts, consider using pipelining:
+
+```csharp
+var batch = db.CreateBatch();
+var tasks = new List>();
+
+foreach (var (member, vector) in vectorData)
+{
+ var request = VectorSetAddRequest.Member(member, vector.AsMemory());
+ tasks.Add(batch.VectorSetAddAsync(key, request));
+}
+
+batch.Execute();
+await Task.WhenAll(tasks);
+```
+
+### Search Optimization
+
+- Use **quantization** to reduce memory usage and improve search speed
+- Tune **SearchExplorationFactor** based on accuracy vs. speed requirements
+- Use **filters** to reduce the search space
+- Consider **dimension reduction** for very high-dimensional vectors
+
+### Range Query Pagination
+
+Prefer enumeration for large result sets to avoid loading everything into memory:
+
+```csharp
+// Good: loads results in batches, processes items individually
+await foreach (var member in db.VectorSetRangeEnumerateAsync(key))
+{
+ await ProcessMemberAsync(member);
+}
+
+// Avoid: loads all results at once
+using var allMembers1 = await db.VectorSetRangeAsync(key);
+
+// Avoid: loads results in batches, but still loads everything into memory at once
+var allMembers2 = await db.VectorSetRangeEnumerateAsync(key).ToArrayAsync();
+```
+
+## Common Patterns
+
+### Semantic Search
+
+```csharp
+// 1. Store document embeddings
+var embedding = await GetEmbeddingFromMLModel(document);
+var request = VectorSetAddRequest.Member(
+ documentId,
+ embedding.AsMemory(),
+ attributesJson: $$"""{"title":"{{document.Title}}","date":"{{document.Date}}"}"""
+);
+await db.VectorSetAddAsync("documents", request);
+
+// 2. Search for similar documents
+var queryEmbedding = await GetEmbeddingFromMLModel(searchQuery);
+var query = VectorSetSimilaritySearchRequest.ByVector(queryEmbedding.AsMemory());
+query.Count = 10;
+query.WithScores = true;
+query.WithAttributes = true;
+
+using var results = await db.VectorSetSimilaritySearchAsync("documents", query);
+```
+
+### Recommendation System
+
+```csharp
+// Find similar items based on an item the user liked
+var query = VectorSetSimilaritySearchRequest.ByMember(userLikedItemId);
+query.Count = 20;
+query.FilterExpression = "$.inStock == true && $.price < 100";
+query.WithScores = true;
+
+using var recommendations = await db.VectorSetSimilaritySearchAsync("products", query);
+```
+
+## See Also
+
+- [Redis Vector Sets Documentation](https://redis.io/docs/latest/develop/data-types/vector-sets/)
+- [HNSW Algorithm](https://arxiv.org/abs/1603.09320)
+- [Filtered Search Syntax](https://redis.io/docs/latest/develop/data-types/vector-sets/filtered-search/)
+
diff --git a/docs/docs.csproj b/docs/docs.csproj
new file mode 100644
index 000000000..977e065bc
--- /dev/null
+++ b/docs/docs.csproj
@@ -0,0 +1,6 @@
+
+
+
+ netstandard2.0
+
+
diff --git a/docs/exp/SER001.md b/docs/exp/SER001.md
new file mode 100644
index 000000000..2def8be6e
--- /dev/null
+++ b/docs/exp/SER001.md
@@ -0,0 +1,22 @@
+At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/):
+
+> Vector set is a new data type that is currently in preview and may be subject to change.
+
+As such, the corresponding library feature must also be considered subject to change:
+
+1. Existing bindings may cease working correctly if the underlying server API changes.
+2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
+ or run-time breaks.
+
+While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
+this warning by adding the following to your `csproj` file:
+
+```xml
+$(NoWarn);SER001
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER001
+```
\ No newline at end of file
diff --git a/docs/exp/SER002.md b/docs/exp/SER002.md
new file mode 100644
index 000000000..d122038e2
--- /dev/null
+++ b/docs/exp/SER002.md
@@ -0,0 +1,26 @@
+Redis 8.4 is currently in preview and may be subject to change.
+
+New features in Redis 8.4 include:
+
+- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry
+- [`XREADGROUP ... CLAIM`](https://github.com/redis/redis/pull/14402) for simplifed stream consumption
+- [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14435) for checked (CAS/CAD) string operations
+
+The corresponding library feature must also be considered subject to change:
+
+1. Existing bindings may cease working correctly if the underlying server API changes.
+2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
+ or run-time breaks.
+
+While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
+this warning by adding the following to your `csproj` file:
+
+```xml
+$(NoWarn);SER002
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER002
+```
diff --git a/docs/exp/SER003.md b/docs/exp/SER003.md
new file mode 100644
index 000000000..651434063
--- /dev/null
+++ b/docs/exp/SER003.md
@@ -0,0 +1,25 @@
+Redis 8.6 is currently in preview and may be subject to change.
+
+New features in Redis 8.6 include:
+
+- `HOTKEYS` for profiling CPU and network hot-spots by key
+- `XADD IDMP[AUTP]` for idempotent (write-at-most-once) stream addition
+
+The corresponding library feature must also be considered subject to change:
+
+1. Existing bindings may cease working correctly if the underlying server API changes.
+2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
+ or run-time breaks.
+
+While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
+this warning by adding the following to your `csproj` file:
+
+```xml
+$(NoWarn);SER003
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER003
+```
diff --git a/docs/index.md b/docs/index.md
index 2fb22443c..0a2e6c721 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,13 +1,13 @@
StackExchange.Redis
===================
-[Release Notes](ReleaseNotes)
+- [Release Notes](ReleaseNotes)
## Overview
StackExchange.Redis is a high performance general purpose redis client for .NET languages (C#, etc.). It is the logical successor to [BookSleeve](https://code.google.com/archive/p/booksleeve/),
-and is the client developed-by (and used-by) [Stack Exchange](http://stackexchange.com/) for busy sites like [Stack Overflow](http://stackoverflow.com/). For the full reasons
-why this library was created (i.e. "What about BookSleeve?") [please see here](http://marcgravell.blogspot.com/2014/03/so-i-went-and-wrote-another-redis-client.html).
+and is the client developed-by (and used-by) [Stack Exchange](https://stackexchange.com/) for busy sites like [Stack Overflow](https://stackoverflow.com/). For the full reasons
+why this library was created (i.e. "What about BookSleeve?") [please see here](https://marcgravell.blogspot.com/2014/03/so-i-went-and-wrote-another-redis-client.html).
Features
--
@@ -31,24 +31,34 @@ Documentation
---
- [Server](Server) - running a redis server
+- [Authentication](Authentication) - connecting to a Redis server with user authentication
- [Basic Usage](Basics) - getting started and basic usage
+- [Async Timeouts](AsyncTimeouts) - async timeouts and cancellation
- [Configuration](Configuration) - options available when connecting to redis
- [Pipelines and Multiplexers](PipelinesMultiplexers) - what is a multiplexer?
- [Keys, Values and Channels](KeysValues) - discusses the data-types used on the API
- [Transactions](Transactions) - how atomic transactions work in redis
+- [Compare-And-Swap / Compare-And-Delete (CAS/CAD)](CompareAndSwap) - atomic conditional operations using value comparison
- [Events](Events) - the events available for logging / information purposes
- [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing
+- [Pub/Sub Key Notifications](KeyspaceNotifications) - how to use keyspace and keyevent notifications
+- [Hot Keys](HotKeys) - how to use `HOTKEYS` profiling
+- [Using RESP3](Resp3) - information on using RESP3
+- [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis)
- [Streams](Streams) - how to use the Stream data type
+- [Vector Sets](VectorSets) - how to use Vector Sets for similarity search with embeddings
- [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands
- [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world
- [Scripting](Scripting) - running Lua scripts with convenient named parameter replacement
- [Testing](Testing) - running the `StackExchange.Redis.Tests` suite to validate changes
+- [Timeouts](Timeouts) - guidance on dealing with timeout problems
- [Thread Theft](ThreadTheft) - guidance on avoiding TPL threading problems
+- [RESP Logging](RespLogging) - capturing and validating RESP streams
Questions and Contributions
---
If you think you have found a bug or have a feature request, please [report an issue][2], or if appropriate: submit a pull request. If you have a question, feel free to [contact me](https://github.com/mgravell).
- [1]: http://msdn.microsoft.com/en-us/library/dd460717%28v=vs.110%29.aspx
+ [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
[2]: https://github.com/StackExchange/StackExchange.Redis/issues?state=open
diff --git a/eng/StackExchange.Redis.Build/AsciiHash.md b/eng/StackExchange.Redis.Build/AsciiHash.md
new file mode 100644
index 000000000..4a76ded62
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/AsciiHash.md
@@ -0,0 +1,173 @@
+# AsciiHash
+
+Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals.
+
+The purpose of this generator is to efficiently interpret input tokens like `bin`, `f32`, etc - whether as byte or character data.
+
+There are multiple ways of using this tool, with the main distinction being whether you are confirming a single
+token, or choosing between multiple tokens (in which case an `enum` is more appropriate):
+
+## Isolated literals (part 1)
+
+When using individual tokens, a `static partial class` can be used to generate helpers:
+
+``` c#
+[AsciiHash] public static partial class bin { }
+[AsciiHash] public static partial class f32 { }
+```
+
+Usually the token is inferred from the name; `[AsciiHash("real value")]` can be used if the token is not a valid identifier.
+Underscores are replaced with hyphens, so a field called `my_token` has the default value `"my-token"`.
+The generator demands *all* of `[AsciiHash] public static partial class`, and note that any *containing* types must
+*also* be declared `partial`.
+
+The output is of the form:
+
+``` c#
+static partial class bin
+{
+ public const int Length = 3;
+ public const long HashCS = ...
+ public const long HashUC = ...
+ public static ReadOnlySpan U8 => @"bin"u8;
+ public static string Text => @"bin";
+ public static bool IsCS(in ReadOnlySpan value, long cs) => ...
+ public static bool IsCI(in RawResult value, long uc) => ...
+
+}
+```
+The `CS` and `UC` are case-sensitive and case-insensitive (using upper-case) tools, respectively.
+
+(this API is strictly an internal implementation detail, and can change at any time)
+
+This generated code allows for fast, efficient, and safe matching of well-known tokens, for example:
+
+``` c#
+var key = ...
+var hash = key.HashCS();
+switch (key.Length)
+{
+ case bin.Length when bin.Is(key, hash):
+ // handle bin
+ break;
+ case f32.Length when f32.Is(key, hash):
+ // handle f32
+ break;
+}
+```
+
+The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler)
+as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches
+must also perform a sequence equality check - the `Is(value, hash)` convenience method validates both hash and equality.
+
+Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties
+that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but
+easy to return via a property.
+
+## Isolated literals (part 2)
+
+In some cases, you want to be able to say "match this value, only known at runtime". For this, note that `AsciiHash`
+is also a `struct` that you can create an instance of and supply to code; the best way to do this is *inside* your
+`partial class`:
+
+``` c#
+[AsciiHash]
+static partial class bin
+{
+ public static readonly AsciiHash Hash = new(U8);
+}
+```
+
+Now, `bin.Hash` can be supplied to a caller that takes an `AsciiHash` instance (commonly with `in` semantics),
+which then has *instance* methods for case-sensitive and case-insensitive matching; the instance already knows
+the target hash and payload values.
+
+The `AsciiHash` returned implements `IEquatable` implementing case-sensitive equality; there are
+also independent case-sensitive and case-insensitive comparers available via the static
+`CaseSensitiveEqualityComparer` and `CaseInsensitiveEqualityComparer` properties respectively.
+
+Comparison values can be constructed on the fly on top of transient buffers using the constructors **that take
+arrays**. Note that the other constructors may allocate on a per-usage basis.
+
+## Enum parsing (part 1)
+
+When identifying multiple values, an `enum` may be more convenient. Consider:
+
+``` c#
+[AsciiHash]
+public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value);
+```
+
+This generates an efficient parser; inputs can be common `byte` or `char` types. Case sensitivity
+is controlled by the optional `CaseSensitive` property on the attribute, or via a 3rd (`bool`) parameter
+bbon the method, i.e.
+
+``` c#
+[AsciiHash(CaseSensitive = false)]
+public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value);
+```
+
+or
+
+``` c#
+[AsciiHash]
+public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value, bool caseSensitive = true);
+```
+
+Individual enum members can also be marked with `[AsciiHash("token value")]` to override the token payload. If
+an enum member declares an empty explicit value (i.e. `[AsciiHash("")]`), then that member is ignored by the
+tool; this is useful for marking "unknown" or "invalid" enum values (commonly the first enum, which by
+convention has the value `0`):
+
+``` c#
+public enum SomeEnum
+{
+ [AsciiHash("")]
+ Unknown,
+ SomeRealValue,
+ [AsciiHash("another-real-value")]
+ AnotherRealValue,
+ // ...
+}
+```
+
+## Enum parsing (part 2)
+
+The tool has an *additional* facility when it comes to enums; you generally don't want to have to hard-code
+things like buffer-lengths into your code, but when parsing an enum, you need to know how many bytes to read.
+
+The tool can generate a `static partial class` that contains the maximum length of any token in the enum, as well
+as the maximum length of any token in bytes (when encoded as UTF-8). For example:
+
+``` c#
+[AsciiHash("SomeTypeName")]
+public enum SomeEnum
+{
+ // ...
+}
+```
+
+This generates a class like the following:
+
+``` c#
+static partial class SomeTypeName
+{
+ public const int EnumCount = 48;
+ public const int MaxChars = 11;
+ public const int MaxBytes = 11; // as UTF8
+ public const int BufferBytes = 16;
+}
+```
+
+The last of these is probably the most useful - it allows an additional byte (to rule out false-positives),
+and rounds up to word-sizes, allowing for convenient stack-allocation - for example:
+
+``` c#
+var span = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(stackalloc byte[SomeTypeName.BufferBytes]);
+if (TryParse(span, out var value))
+{
+ // got a value
+}
+```
+
+which allows for very efficient parsing of well-known tokens.
\ No newline at end of file
diff --git a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs
new file mode 100644
index 000000000..4fb411454
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs
@@ -0,0 +1,774 @@
+using System.Buffers;
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using RESPite;
+
+namespace StackExchange.Redis.Build;
+
+[Generator(LanguageNames.CSharp)]
+public class AsciiHashGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // looking for [AsciiHash] partial static class Foo { }
+ var types = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (node, _) => node is ClassDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) &&
+ HasAsciiHash(decl.AttributeLists),
+ TransformTypes)
+ .Where(pair => pair.Name is { Length: > 0 })
+ .Collect();
+
+ // looking for [AsciiHash] partial static bool TryParse(input, out output) { }
+ var methods = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (node, _) => node is MethodDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) &&
+ HasAsciiHash(decl.AttributeLists),
+ TransformMethods)
+ .Where(pair => pair.Name is { Length: > 0 })
+ .Collect();
+
+ // looking for [AsciiHash("some type")] enum Foo { }
+ var enums = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (node, _) => node is EnumDeclarationSyntax decl && HasAsciiHash(decl.AttributeLists),
+ TransformEnums)
+ .Where(pair => pair.Name is { Length: > 0 })
+ .Collect();
+
+ context.RegisterSourceOutput(
+ types.Combine(methods).Combine(enums),
+ (ctx, content) =>
+ Generate(ctx, content.Left.Left, content.Left.Right, content.Right));
+
+ static bool IsStaticPartial(SyntaxTokenList tokens)
+ => tokens.Any(SyntaxKind.StaticKeyword) && tokens.Any(SyntaxKind.PartialKeyword);
+
+ static bool HasAsciiHash(SyntaxList attributeLists)
+ {
+ foreach (var attribList in attributeLists)
+ {
+ foreach (var attrib in attribList.Attributes)
+ {
+ if (attrib.Name.ToString() is nameof(AsciiHashAttribute) or nameof(AsciiHash)) return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private static string GetName(INamedTypeSymbol type)
+ {
+ if (type.ContainingType is null) return type.Name;
+ var stack = new Stack();
+ while (true)
+ {
+ stack.Push(type.Name);
+ if (type.ContainingType is null) break;
+ type = type.ContainingType;
+ }
+
+ var sb = new StringBuilder(stack.Pop());
+ while (stack.Count != 0)
+ {
+ sb.Append('.').Append(stack.Pop());
+ }
+
+ return sb.ToString();
+ }
+
+ private static AttributeData? TryGetAsciiHashAttribute(ImmutableArray attributes)
+ {
+ foreach (var attrib in attributes)
+ {
+ if (attrib.AttributeClass is
+ {
+ Name: nameof(AsciiHashAttribute),
+ ContainingType: null,
+ ContainingNamespace:
+ {
+ Name: "RESPite",
+ ContainingNamespace.IsGlobalNamespace: true,
+ }
+ })
+ {
+ return attrib;
+ }
+ }
+
+ return null;
+ }
+
+ private (string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes) TransformEnums(
+ GeneratorSyntaxContext ctx, CancellationToken cancellationToken)
+ {
+ // extract the name and value (defaults to name, but can be overridden via attribute) and the location
+ if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol { TypeKind: TypeKind.Enum } named) return default;
+ if (TryGetAsciiHashAttribute(named.GetAttributes()) is not { } attrib) return default;
+ var innerName = GetRawValue("", attrib);
+ if (string.IsNullOrWhiteSpace(innerName)) return default;
+
+ string ns = "", parentType = "";
+ if (named.ContainingType is { } containingType)
+ {
+ parentType = GetName(containingType);
+ ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+ else if (named.ContainingNamespace is { } containingNamespace)
+ {
+ ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+
+ int maxChars = 0, maxBytes = 0, count = 0;
+ foreach (var member in named.GetMembers())
+ {
+ if (member.Kind is SymbolKind.Field)
+ {
+ var rawValue = GetRawValue(member.Name, TryGetAsciiHashAttribute(member.GetAttributes()));
+ if (string.IsNullOrWhiteSpace(rawValue)) continue;
+
+ count++;
+ maxChars = Math.Max(maxChars, rawValue.Length);
+ maxBytes = Math.Max(maxBytes, Encoding.UTF8.GetByteCount(rawValue));
+ }
+ }
+ return (ns, parentType, innerName, count, maxChars, maxBytes);
+ }
+
+ private (string Namespace, string ParentType, string Name, string Value) TransformTypes(
+ GeneratorSyntaxContext ctx,
+ CancellationToken cancellationToken)
+ {
+ // extract the name and value (defaults to name, but can be overridden via attribute) and the location
+ if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol { TypeKind: TypeKind.Class } named) return default;
+ if (TryGetAsciiHashAttribute(named.GetAttributes()) is not { } attrib) return default;
+
+ string ns = "", parentType = "";
+ if (named.ContainingType is { } containingType)
+ {
+ parentType = GetName(containingType);
+ ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+ else if (named.ContainingNamespace is { } containingNamespace)
+ {
+ ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+
+ string name = named.Name, value = GetRawValue(name, attrib);
+ if (string.IsNullOrWhiteSpace(value)) return default;
+ return (ns, parentType, name, value);
+ }
+
+ private static string GetRawValue(string name, AttributeData? asciiHashAttribute)
+ {
+ var value = "";
+ if (asciiHashAttribute is { ConstructorArguments.Length: 1 }
+ && asciiHashAttribute.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val)
+ {
+ value = val;
+ }
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ value = InferPayload(name); // if nothing explicit: infer from name
+ }
+
+ return value;
+ }
+
+ private static string InferPayload(string name) => name.Replace("_", "-");
+
+ private (string Namespace, string ParentType, Accessibility Accessibility, string Name,
+ (string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To,
+ (string Name, bool Value, RefKind RefKind) CaseSensitive,
+ BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue) TransformMethods(
+ GeneratorSyntaxContext ctx,
+ CancellationToken cancellationToken)
+ {
+ if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol
+ {
+ IsStatic: true,
+ IsPartialDefinition: true,
+ PartialImplementationPart: null,
+ Arity: 0,
+ ReturnType.SpecialType: SpecialType.System_Boolean,
+ Parameters:
+ {
+ IsDefaultOrEmpty: false,
+ Length: 2 or 3,
+ },
+ } method) return default;
+
+ if (TryGetAsciiHashAttribute(method.GetAttributes()) is not { } attrib) return default;
+
+ if (method.ContainingType is not { } containingType) return default;
+ var parentType = GetName(containingType);
+ var ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ var arg = method.Parameters[0];
+ if (arg is not { IsOptional: false, RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter }) return default;
+
+ static bool IsBytes(ITypeSymbol type)
+ {
+ // byte[]
+ if (type is IArrayTypeSymbol { ElementType: { SpecialType: SpecialType.System_Byte } })
+ return true;
+
+ // Span or ReadOnlySpan
+ if (type is INamedTypeSymbol { TypeKind: TypeKind.Struct, Arity: 1, Name: "Span" or "ReadOnlySpan",
+ ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true },
+ TypeArguments: { Length: 1 } ta }
+ && ta[0].SpecialType == SpecialType.System_Byte)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ var fromType = arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ bool fromBytes = IsBytes(arg.Type);
+ var from = (fromType, arg.Name, fromBytes, arg.RefKind);
+
+ arg = method.Parameters[1];
+ if (arg is not
+ {
+ IsOptional: false, RefKind: RefKind.Out or RefKind.Ref, Type: INamedTypeSymbol { TypeKind: TypeKind.Enum }
+ }) return default;
+ var to = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind);
+
+ var members = arg.Type.GetMembers();
+ var builder = new BasicArray<(string EnumMember, string ParseText)>.Builder(members.Length);
+ HashSet values = new();
+ foreach (var member in members)
+ {
+ if (member is IFieldSymbol { IsStatic: true, IsConst: true } field)
+ {
+ var rawValue = GetRawValue(field.Name, TryGetAsciiHashAttribute(member.GetAttributes()));
+ if (string.IsNullOrWhiteSpace(rawValue)) continue;
+ builder.Add((field.Name, rawValue));
+ int value = field.ConstantValue switch
+ {
+ sbyte i8 => i8,
+ short i16 => i16,
+ int i32 => i32,
+ long i64 => (int)i64,
+ byte u8 => u8,
+ ushort u16 => u16,
+ uint u32 => (int)u32,
+ ulong u64 => (int)u64,
+ char c16 => c16,
+ _ => 0,
+ };
+ values.Add(value);
+ }
+ }
+
+ (string, bool, RefKind) caseSensitive;
+ bool cs = IsCaseSensitive(attrib);
+ if (method.Parameters.Length > 2)
+ {
+ arg = method.Parameters[2];
+ if (arg is not
+ {
+ RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter,
+ Type.SpecialType: SpecialType.System_Boolean,
+ })
+ {
+ return default;
+ }
+
+ if (arg.IsOptional)
+ {
+ if (arg.ExplicitDefaultValue is not bool dv) return default;
+ cs = dv;
+ }
+ caseSensitive = (arg.Name, cs, arg.RefKind);
+ }
+ else
+ {
+ caseSensitive = ("", cs, RefKind.None);
+ }
+
+ int defaultValue = 0;
+ if (values.Contains(0))
+ {
+ int len = values.Count;
+ for (int i = 1; i <= len; i++)
+ {
+ if (!values.Contains(i))
+ {
+ defaultValue = i;
+ break;
+ }
+ }
+ }
+ return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, caseSensitive, builder.Build(), defaultValue);
+ }
+
+ private bool IsCaseSensitive(AttributeData attrib)
+ {
+ foreach (var member in attrib.NamedArguments)
+ {
+ if (member.Key == nameof(AsciiHashAttribute.CaseSensitive)
+ && member.Value.Kind is TypedConstantKind.Primitive
+ && member.Value.Value is bool caseSensitive)
+ {
+ return caseSensitive;
+ }
+ }
+
+ return true;
+ }
+
+ private string GetVersion()
+ {
+ var asm = GetType().Assembly;
+ if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is
+ AssemblyFileVersionAttribute { Version: { Length: > 0 } } version)
+ {
+ return version.Version;
+ }
+
+ return asm.GetName().Version?.ToString() ?? "??";
+ }
+
+ private void Generate(
+ SourceProductionContext ctx,
+ ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> types,
+ ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name,
+ (string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To,
+ (string Name, bool Value, RefKind RefKind) CaseSensitive,
+ BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> parseMethods,
+ ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums)
+ {
+ if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do
+
+ var sb = new StringBuilder("// ")
+ .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();
+
+ sb.AppendLine("using System;");
+ sb.AppendLine("using StackExchange.Redis;");
+ sb.AppendLine("#pragma warning disable CS8981, SER004");
+
+ BuildTypeImplementations(sb, types);
+ BuildEnumParsers(sb, parseMethods);
+ BuildEnumLengths(sb, enums);
+ ctx.AddSource(nameof(AsciiHash) + ".generated.cs", sb.ToString());
+ }
+
+ private void BuildEnumLengths(StringBuilder sb, ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums)
+ {
+ if (enums.IsDefaultOrEmpty) return; // nope
+
+ int indent = 0;
+ StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
+
+ foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType)))
+ {
+ NewLine();
+ int braces = 0;
+ if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
+ {
+ NewLine().Append("namespace ").Append(grp.Key.Namespace);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+
+ if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
+ {
+ if (grp.Key.ParentType.Contains('.')) // nested types
+ {
+ foreach (var part in grp.Key.ParentType.Split('.'))
+ {
+ NewLine().Append("partial class ").Append(part);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+ else
+ {
+ NewLine().Append("partial class ").Append(grp.Key.ParentType);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+
+ foreach (var @enum in grp)
+ {
+ NewLine().Append("internal static partial class ").Append(@enum.Name);
+ NewLine().Append("{");
+ indent++;
+ NewLine().Append("public const int EnumCount = ").Append(@enum.Count).Append(";");
+ NewLine().Append("public const int MaxChars = ").Append(@enum.MaxChars).Append(";");
+ NewLine().Append("public const int MaxBytes = ").Append(@enum.MaxBytes).Append("; // as UTF8");
+ // for buffer bytes: we want to allow 1 extra byte (to check for false-positive over-long values),
+ // and then round up to the nearest multiple of 8 (for stackalloc performance, etc)
+ int bufferBytes = (@enum.MaxBytes + 1 + 7) & ~7;
+ NewLine().Append("public const int BufferBytes = ").Append(bufferBytes).Append(";");
+ indent--;
+ NewLine().Append("}");
+ }
+
+ // handle any closing braces
+ while (braces-- > 0)
+ {
+ indent--;
+ NewLine().Append("}");
+ }
+ }
+ }
+
+ private void BuildEnumParsers(
+ StringBuilder sb,
+ in ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name,
+ (string Type, string Name, bool IsBytes, RefKind RefKind) From,
+ (string Type, string Name, RefKind RefKind) To,
+ (string Name, bool Value, RefKind RefKind) CaseSensitive,
+ BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> enums)
+ {
+ if (enums.IsDefaultOrEmpty) return; // nope
+
+ int indent = 0;
+ StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
+
+ foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType)))
+ {
+ NewLine();
+ int braces = 0;
+ if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
+ {
+ NewLine().Append("namespace ").Append(grp.Key.Namespace);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+
+ if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
+ {
+ if (grp.Key.ParentType.Contains('.')) // nested types
+ {
+ foreach (var part in grp.Key.ParentType.Split('.'))
+ {
+ NewLine().Append("partial class ").Append(part);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+ else
+ {
+ NewLine().Append("partial class ").Append(grp.Key.ParentType);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+
+ foreach (var method in grp)
+ {
+ var line = NewLine().Append(Format(method.Accessibility)).Append(" static partial bool ")
+ .Append(method.Name).Append("(")
+ .Append(Format(method.From.RefKind))
+ .Append(method.From.Type).Append(" ").Append(method.From.Name).Append(", ")
+ .Append(Format(method.To.RefKind))
+ .Append(method.To.Type).Append(" ").Append(method.To.Name);
+ if (!string.IsNullOrEmpty(method.CaseSensitive.Name))
+ {
+ line.Append(", ").Append(Format(method.CaseSensitive.RefKind)).Append("bool ")
+ .Append(method.CaseSensitive.Name);
+ }
+ line.Append(")");
+ NewLine().Append("{");
+ indent++;
+ NewLine().Append("// ").Append(method.To.Type).Append(" has ").Append(method.Members.Length).Append(" members");
+ string valueTarget = method.To.Name;
+ if (method.To.RefKind != RefKind.Out)
+ {
+ valueTarget = "__tmp";
+ NewLine().Append(method.To.Type).Append(" ").Append(valueTarget).Append(";");
+ }
+
+ bool alwaysCaseSensitive =
+ string.IsNullOrEmpty(method.CaseSensitive.Name) && method.CaseSensitive.Value;
+ if (!alwaysCaseSensitive && !HasCaseSensitiveCharacters(method.Members))
+ {
+ alwaysCaseSensitive = true;
+ }
+
+ bool twoPart = method.Members.Max(x => x.ParseText.Length) > AsciiHash.MaxBytesHashed;
+ if (alwaysCaseSensitive)
+ {
+ if (twoPart)
+ {
+ NewLine().Append("global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(", out var cs0, out var cs1);");
+ }
+ else
+ {
+ NewLine().Append("var cs0 = global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(");");
+ }
+ }
+ else
+ {
+ if (twoPart)
+ {
+ NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name)
+ .Append(", out var cs0, out var uc0, out var cs1, out var uc1);");
+ }
+ else
+ {
+ NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name)
+ .Append(", out var cs0, out var uc0);");
+ }
+ }
+
+ if (string.IsNullOrEmpty(method.CaseSensitive.Name))
+ {
+ Write(method.CaseSensitive.Value);
+ }
+ else
+ {
+ NewLine().Append("if (").Append(method.CaseSensitive.Name).Append(")");
+ NewLine().Append("{");
+ indent++;
+ Write(true);
+ indent--;
+ NewLine().Append("}");
+ NewLine().Append("else");
+ NewLine().Append("{");
+ indent++;
+ Write(false);
+ indent--;
+ NewLine().Append("}");
+ }
+
+ if (method.To.RefKind == RefKind.Out)
+ {
+ NewLine().Append("if (").Append(valueTarget).Append(" == (")
+ .Append(method.To.Type).Append(")").Append(method.DefaultValue).Append(")");
+ NewLine().Append("{");
+ indent++;
+ NewLine().Append("// by convention, init to zero on miss");
+ NewLine().Append(valueTarget).Append(" = default;");
+ NewLine().Append("return false;");
+ indent--;
+ NewLine().Append("}");
+ NewLine().Append("return true;");
+ }
+ else
+ {
+ NewLine().Append("// do not update parameter on miss");
+ NewLine().Append("if (").Append(valueTarget).Append(" == (")
+ .Append(method.To.Type).Append(")").Append(method.DefaultValue).Append(") return false;");
+ NewLine().Append(method.To.Name).Append(" = ").Append(valueTarget).Append(";");
+ NewLine().Append("return true;");
+ }
+
+ void Write(bool caseSensitive)
+ {
+ NewLine().Append(valueTarget).Append(" = ").Append(method.From.Name).Append(".Length switch {");
+ indent++;
+ foreach (var member in method.Members
+ .OrderBy(x => x.ParseText.Length)
+ .ThenBy(x => x.ParseText))
+ {
+ var len = member.ParseText.Length;
+ AsciiHash.Hash(member.ParseText, out var cs0, out var uc0, out var cs1, out var uc1);
+
+ bool valueCaseSensitive = caseSensitive || !HasCaseSensitiveCharacters(member.ParseText);
+
+ line = NewLine().Append(len).Append(" when ");
+ if (twoPart) line.Append("(");
+ if (valueCaseSensitive)
+ {
+ line.Append("cs0 is ").Append(cs0);
+ }
+ else
+ {
+ line.Append("uc0 is ").Append(uc0);
+ }
+
+ if (len > AsciiHash.MaxBytesHashed)
+ {
+ if (valueCaseSensitive)
+ {
+ line.Append(" & cs1 is ").Append(cs1);
+ }
+ else
+ {
+ line.Append(" & uc1 is ").Append(uc1);
+ }
+ }
+ if (twoPart) line.Append(")");
+ if (len > 2 * AsciiHash.MaxBytesHashed)
+ {
+ line.Append(" && ");
+ var csValue = SyntaxFactory
+ .LiteralExpression(
+ SyntaxKind.StringLiteralExpression,
+ SyntaxFactory.Literal(member.ParseText.Substring(2 * AsciiHash.MaxBytesHashed)))
+ .ToFullString();
+
+ line.Append("global::RESPite.AsciiHash.")
+ .Append(valueCaseSensitive ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI))
+ .Append("(").Append(method.From.Name).Append(".Slice(").Append(2 * AsciiHash.MaxBytesHashed).Append("), ").Append(csValue);
+ if (method.From.IsBytes) line.Append("u8");
+ line.Append(")");
+ }
+
+ line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(",");
+ }
+
+ NewLine().Append("_ => (").Append(method.To.Type).Append(")").Append(method.DefaultValue)
+ .Append(",");
+ indent--;
+ NewLine().Append("};");
+ }
+
+ indent--;
+ NewLine().Append("}");
+ }
+
+ // handle any closing braces
+ while (braces-- > 0)
+ {
+ indent--;
+ NewLine().Append("}");
+ }
+ }
+ }
+
+ private static bool HasCaseSensitiveCharacters(string value)
+ {
+ foreach (char c in value ?? "")
+ {
+ if (char.IsLetter(c)) return true;
+ }
+
+ return false;
+ }
+
+ private static bool HasCaseSensitiveCharacters(BasicArray<(string EnumMember, string ParseText)> members)
+ {
+ // do we have alphabet characters? case sensitivity doesn't apply if not
+ foreach (var member in members)
+ {
+ if (HasCaseSensitiveCharacters(member.ParseText)) return true;
+ }
+
+ return false;
+ }
+
+ private static string Format(RefKind refKind) => refKind switch
+ {
+ RefKind.None => "",
+ RefKind.In => "in ",
+ RefKind.Out => "out ",
+ RefKind.Ref => "ref ",
+ RefKind.RefReadOnlyParameter or RefKind.RefReadOnly => "ref readonly ",
+ _ => throw new NotSupportedException($"RefKind {refKind} is not yet supported."),
+ };
+ private static string Format(Accessibility accessibility) => accessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Private => "private",
+ Accessibility.Internal => "internal",
+ Accessibility.Protected => "protected",
+ Accessibility.ProtectedAndInternal => "private protected",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ _ => throw new NotSupportedException($"Accessibility {accessibility} is not yet supported."),
+ };
+
+ private static void BuildTypeImplementations(
+ StringBuilder sb,
+ in ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> types)
+ {
+ if (types.IsDefaultOrEmpty) return; // nope
+
+ int indent = 0;
+ StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
+
+ foreach (var grp in types.GroupBy(l => (l.Namespace, l.ParentType)))
+ {
+ NewLine();
+ int braces = 0;
+ if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
+ {
+ NewLine().Append("namespace ").Append(grp.Key.Namespace);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+
+ if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
+ {
+ if (grp.Key.ParentType.Contains('.')) // nested types
+ {
+ foreach (var part in grp.Key.ParentType.Split('.'))
+ {
+ NewLine().Append("partial class ").Append(part);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+ else
+ {
+ NewLine().Append("partial class ").Append(grp.Key.ParentType);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+
+ foreach (var literal in grp)
+ {
+ // perform string escaping on the generated value (this includes the quotes, note)
+ var csValue = SyntaxFactory
+ .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value))
+ .ToFullString();
+
+ AsciiHash.Hash(literal.Value, out var hashCS, out var hashUC);
+ NewLine().Append("static partial class ").Append(literal.Name);
+ NewLine().Append("{");
+ indent++;
+ NewLine().Append("public const int Length = ").Append(literal.Value.Length).Append(';');
+ NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';');
+ NewLine().Append("public const long HashUC = ").Append(hashUC).Append(';');
+ NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;");
+ NewLine().Append("public const string Text = ").Append(csValue).Append(';');
+ if (literal.Value.Length <= AsciiHash.MaxBytesHashed)
+ {
+ // the case-sensitive hash enforces all the values
+ NewLine().Append(
+ "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS & value.Length == Length;");
+ NewLine().Append(
+ "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC & value.Length == Length;");
+ }
+ else
+ {
+ NewLine().Append(
+ "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS && value.SequenceEqual(U8);");
+ NewLine().Append(
+ "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC && global::RESPite.AsciiHash.SequenceEqualsCI(value, U8);");
+ }
+
+ indent--;
+ NewLine().Append("}");
+ }
+
+ // handle any closing braces
+ while (braces-- > 0)
+ {
+ indent--;
+ NewLine().Append("}");
+ }
+ }
+ }
+}
diff --git a/eng/StackExchange.Redis.Build/BasicArray.cs b/eng/StackExchange.Redis.Build/BasicArray.cs
new file mode 100644
index 000000000..dc7984c75
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/BasicArray.cs
@@ -0,0 +1,85 @@
+using System.Collections;
+
+namespace StackExchange.Redis.Build;
+
+// like ImmutableArray, but with decent equality semantics
+public readonly struct BasicArray : IEquatable>, IReadOnlyList
+{
+ private readonly T[] _elements;
+
+ private BasicArray(T[] elements, int length)
+ {
+ _elements = elements;
+ Length = length;
+ }
+
+ private static readonly EqualityComparer _comparer = EqualityComparer.Default;
+
+ public int Length { get; }
+ public bool IsEmpty => Length == 0;
+
+ public ref readonly T this[int index]
+ {
+ get
+ {
+ if (index < 0 | index >= Length) Throw();
+ return ref _elements[index];
+
+ static void Throw() => throw new IndexOutOfRangeException();
+ }
+ }
+
+ public ReadOnlySpan Span => _elements.AsSpan(0, Length);
+
+ public bool Equals(BasicArray other)
+ {
+ if (Length != other.Length) return false;
+ var y = other.Span;
+ int i = 0;
+ foreach (ref readonly T el in this.Span)
+ {
+ if (!_comparer.Equals(el, y[i])) return false;
+ }
+
+ return true;
+ }
+
+ public ReadOnlySpan.Enumerator GetEnumerator() => Span.GetEnumerator();
+
+ private IEnumerator EnumeratorCore()
+ {
+ for (int i = 0; i < Length; i++) yield return this[i];
+ }
+
+ public override bool Equals(object? obj) => obj is BasicArray other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ var hash = Length;
+ foreach (ref readonly T el in this.Span)
+ {
+ _ = (hash * -37) + _comparer.GetHashCode(el);
+ }
+
+ return hash;
+ }
+ IEnumerator IEnumerable.GetEnumerator() => EnumeratorCore();
+ IEnumerator IEnumerable.GetEnumerator() => EnumeratorCore();
+
+ int IReadOnlyCollection.Count => Length;
+ T IReadOnlyList.this[int index] => this[index];
+
+ public struct Builder(int maxLength)
+ {
+ public int Count { get; private set; }
+ private readonly T[] elements = maxLength == 0 ? [] : new T[maxLength];
+
+ public void Add(in T value)
+ {
+ elements[Count] = value;
+ Count++;
+ }
+
+ public BasicArray Build() => new(elements, Count);
+ }
+}
diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj
new file mode 100644
index 000000000..3cde6f5f6
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ Shared/AsciiHash.cs
+
+
+ Shared/Experiments.cs
+
+
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 93251be41..27366ae98 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,10 +3,12 @@
truetrue
+ false
+ true
-
-
-
-
+
+
+
+
diff --git a/src/NRediSearch/AddOptions.cs b/src/NRediSearch/AddOptions.cs
deleted file mode 100644
index bd3fb9d20..000000000
--- a/src/NRediSearch/AddOptions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-// .NET port of https://github.com/RedisLabs/JRediSearch/
-
-namespace NRediSearch
-{
- public sealed class AddOptions
- {
- public enum ReplacementPolicy
- {
- ///
- /// The default mode. This will cause the add operation to fail if the document already exists
- ///
- None,
- ///
- /// Replace/reindex the entire document. This has the effect of atomically deleting the previous
- /// document and replacing it with the context of the new document. Fields in the old document which
- /// are not present in the new document are lost
- ///
- Full,
- ///
- /// Only reindex/replace fields that are updated in the command. Fields in the old document which are
- /// not present in the new document are preserved.Fields that are present in both are overwritten by
- /// the new document
- ///
- Partial,
- }
-
- public string Language { get; set; }
- public bool NoSave { get; set; }
- public ReplacementPolicy ReplacePolicy { get; set; }
-
- ///
- /// Create a new DocumentOptions object. Methods can later be chained via a builder-like pattern
- ///
- public AddOptions() { }
-
- ///
- /// Set the indexing language
- ///
- /// Set the indexing language
- public AddOptions SetLanguage(string language)
- {
- Language = language;
- return this;
- }
- ///
- /// Whether document's contents should not be stored in the database.
- ///
- /// if enabled, the document is not stored on the server. This saves disk/memory space on the
- /// server but prevents retrieving the document itself.
- public AddOptions SetNoSave(bool enabled)
- {
- NoSave = enabled;
- return this;
- }
-
- ///
- /// Indicate the behavior for the existing document.
- ///
- /// One of the replacement modes.
- public AddOptions SetReplacementPolicy(ReplacementPolicy mode)
- {
- ReplacePolicy = mode;
- return this;
- }
- }
-}
diff --git a/src/NRediSearch/Aggregation/AggregationBuilder.cs b/src/NRediSearch/Aggregation/AggregationBuilder.cs
deleted file mode 100644
index f3dc4b47f..000000000
--- a/src/NRediSearch/Aggregation/AggregationBuilder.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-// .NET port of https://github.com/RedisLabs/JRediSearch/
-using System.Collections.Generic;
-using System.Linq;
-using NRediSearch.Aggregation.Reducers;
-
-namespace NRediSearch.Aggregation
-{
- public sealed class AggregationBuilder
- {
- private readonly List