Add Redis support for SignalR backplane and caching

- Introduced Redis configuration in appsettings.json to enable SignalR backplane functionality.
- Updated Program.cs to conditionally configure SignalR with Redis if a connection string is provided.
- Added Redis connection service registration in ApiBootstrap for distributed scenarios.
- Included necessary package references for StackExchange.Redis and Microsoft.Extensions.Caching.StackExchangeRedis in project files.
- Implemented password masking for Redis connection strings to enhance security.
This commit is contained in:
2026-01-07 16:59:10 +07:00
parent bc4725ca19
commit 7108907e0e
10 changed files with 847 additions and 1 deletions

243
REDIS_SIGNALR_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,243 @@
# Redis + SignalR Multi-Instance Deployment Guide
## Summary
The Managing API now supports **multiple instances** with **SignalR** (for LlmHub, BotHub, BacktestHub) using a **Redis backplane**.
This solves the "No Connection with that ID" error that occurs when:
- `/llmhub/negotiate` hits instance A
- WebSocket connection hits instance B (which doesn't know about the connection ID)
## What Was Added
### 1. Infrastructure Layer - Generic Redis Service
**Files Created:**
- `src/Managing.Application.Abstractions/Services/IRedisConnectionService.cs` - Interface
- `src/Managing.Infrastructure.Storage/RedisConnectionService.cs` - Implementation
- `src/Managing.Infrastructure.Storage/README-REDIS.md` - Documentation
**Purpose:** Generic Redis connectivity that can be used for SignalR, caching, or any Redis needs.
### 2. SignalR Redis Backplane
**Files Modified:**
- `src/Managing.Api/Program.cs` - Auto-configures SignalR with Redis when available
- `src/Managing.Bootstrap/ApiBootstrap.cs` - Registers Redis service
**How It Works:**
- Checks if Redis is configured
- If yes: Adds Redis backplane to SignalR
- If no: Runs in single-instance mode (graceful degradation)
### 3. Configuration
**Files Modified:**
- `src/Managing.Api/appsettings.json` - Default config (empty, for local dev)
- `src/Managing.Api/appsettings.Sandbox.json` - `srv-captain--redis:6379`
- `src/Managing.Api/appsettings.Production.json` - `srv-captain--redis:6379`
### 4. NuGet Packages Added
- `Microsoft.AspNetCore.SignalR.StackExchangeRedis` (8.0.10) - SignalR backplane
- `Microsoft.Extensions.Caching.StackExchangeRedis` (8.0.10) - Redis caching
- `StackExchange.Redis` (2.8.16) - Redis client
## Deployment Steps for CapRover
### Step 1: Create Redis Service
1. In CapRover, go to **Apps**
2. Click **One-Click Apps/Databases**
3. Search for "Redis"
4. Deploy Redis (or use existing one)
5. Note the service name: `srv-captain--redis` (or your custom name)
### Step 2: Configure CapRover App
For `dev-managing-api` (Sandbox):
1. **Enable WebSocket Support**
- Go to **HTTP Settings**
- Toggle **"WebSocket Support"** to ON
- Save
2. **Enable Sticky Sessions**
- In **HTTP Settings**
- Toggle **"Enable Sticky Sessions"** to ON
- Save
3. **Verify Redis Connection String**
- The connection string is already in `appsettings.Sandbox.json`
- Default: `srv-captain--redis:6379`
- If you used a different Redis service name, update via environment variable:
```
ConnectionStrings__Redis=srv-captain--your-redis-name:6379
```
- Or use the fallback:
```
REDIS_URL=srv-captain--your-redis-name:6379
```
### Step 3: Deploy
1. Build and deploy the API:
```bash
cd src/Managing.Api
# Your normal deployment process
```
2. Watch the logs during startup. You should see:
```
✅ Configuring SignalR with Redis backplane: srv-captain--redis:6379
✅ Redis connection established successfully
```
### Step 4: Scale to Multiple Instances
1. In CapRover, go to your `dev-managing-api` app
2. **App Configs** tab
3. Set **"Number of app instances"** to `2` or `3`
4. Click **Save & Update**
### Step 5: Test
1. Open the frontend (Kaigen Web UI)
2. Open the AI Chat
3. Send a message
4. Should work without "No Connection with that ID" errors
## Verification Checklist
After deployment, verify:
- [ ] Redis service is running in CapRover
- [ ] WebSocket support is enabled
- [ ] Sticky sessions are enabled
- [ ] API logs show Redis connection success
- [ ] Multiple instances are running
- [ ] AI Chat works without connection errors
- [ ] Browser Network tab shows WebSocket upgrade successful
## Troubleshooting
### Issue: "No Connection with that ID" Still Appears
**Check:**
1. Redis service is running: `redis-cli -h srv-captain--redis ping`
2. API logs show Redis connected (not "Redis not configured")
3. Sticky sessions are ON
4. WebSocket support is ON
**Quick Test:**
- Temporarily set instances to 1
- If it works with 1 instance, the issue is multi-instance setup
- If it fails with 1 instance, check WebSocket/proxy configuration
### Issue: Redis Connection Failed
**Check Logs For:**
```
⚠️ Failed to configure SignalR Redis backplane: <error>
SignalR will work in single-instance mode only
```
**Solutions:**
1. Verify Redis service name matches configuration
2. Ensure Redis is not password-protected (or add password to config)
3. Check Redis service health in CapRover
### Issue: WebSocket Upgrade Failed
Not related to Redis. Check:
1. CapRover WebSocket support is ON
2. Nginx configuration allows upgrades
3. Browser console for detailed error
## Configuration Reference
### Connection String Formats
**Simple (no password):**
```
srv-captain--redis:6379
```
**With Password:**
```
srv-captain--redis:6379,password=your-password
```
**Multiple Options:**
```
srv-captain--redis:6379,password=pwd,ssl=true,abortConnect=false
```
### Configuration Priority
The app checks these in order:
1. `ConnectionStrings:Redis` (appsettings.json or `ConnectionStrings__Redis` environment variable)
2. `REDIS_URL` (fallback environment variable)
**Recommended**: Use `ConnectionStrings__Redis` environment variable to override appsettings without rebuilding.
## Architecture Benefits
### Before (Single Instance)
```
Frontend → Nginx → API Instance
- In-memory SignalR
- Connection IDs stored locally
❌ Scale limited to 1 instance
```
### After (Multi-Instance with Redis)
```
Frontend → Nginx (sticky) → API Instance 1 ┐
→ API Instance 2 ├─→ Redis ← SignalR Backplane
→ API Instance 3 ┘
- Connection IDs in Redis
- Messages distributed via pub/sub
- Any instance can handle any connection
✅ Scale to N instances
```
## Next Steps
After successful deployment:
1. **Monitor Performance**
- Watch Redis memory usage
- Check API response times
- Monitor WebSocket connection stability
2. **Consider Redis Clustering**
- For high availability
- If scaling beyond 3-4 API instances
3. **Extend Redis Usage**
- Distributed caching
- Rate limiting
- Session storage
## Rollback Plan
If issues occur:
1. **Immediate**: Set instances to 1
2. **Environment Variable**: Set `REDIS_URL=` (empty) to disable Redis
3. **Code Rollback**: Previous version still works (graceful degradation)
The implementation is backward-compatible and doesn't require Redis to function.
## Support
For issues:
1. Check logs: `src/Managing.Infrastructure.Storage/README-REDIS.md`
2. Review this guide
3. Check CapRover app logs for Redis/SignalR messages
4. Test with 1 instance first, then scale up

View File

@@ -17,6 +17,7 @@
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.10" />
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />

View File

@@ -461,7 +461,32 @@ builder.Services.AddCors(options =>
});
});
builder.Services.AddSignalR().AddJsonProtocol();
// Configure SignalR with Redis backplane if available
var signalRBuilder = builder.Services.AddSignalR().AddJsonProtocol();
// Check if Redis is configured for SignalR backplane
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
var redisConnectionString = builder.Configuration.GetConnectionString("Redis")
?? builder.Configuration["REDIS_URL"];
if (!string.IsNullOrWhiteSpace(redisConnectionString))
{
try
{
Console.WriteLine($"✅ Configuring SignalR with Redis backplane");
signalRBuilder.AddStackExchangeRedis(redisConnectionString, options =>
{
// Configure channel prefix for SignalR messages
options.Configuration.ChannelPrefix = "managing-signalr";
});
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Failed to configure SignalR Redis backplane: {ex.Message}");
Console.WriteLine("SignalR will work in single-instance mode only");
}
}
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
builder.Services.RegisterApiDependencies(builder.Configuration);

View File

@@ -23,6 +23,9 @@
"DebitEndpoint": "/api/credits/debit",
"RefundEndpoint": "/api/credits/refund"
},
"ConnectionStrings": {
"Redis": ""
},
"Flagsmith": {
"ApiUrl": "https://flag.kaigen.ai/api/v1/"
},

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,35 @@
using StackExchange.Redis;
namespace Managing.Application.Abstractions.Services;
/// <summary>
/// Service for managing Redis connections in a generic way across the application.
/// This service provides access to the underlying Redis connection multiplexer
/// which can be used for various purposes including SignalR backplane, caching, etc.
/// </summary>
public interface IRedisConnectionService
{
/// <summary>
/// Gets the Redis connection multiplexer instance.
/// Returns null if Redis is not configured or connection failed.
/// </summary>
IConnectionMultiplexer? GetConnection();
/// <summary>
/// Gets a Redis database instance.
/// </summary>
/// <param name="db">Database number (default: -1 for default database)</param>
/// <returns>IDatabase instance or null if not connected</returns>
IDatabase? GetDatabase(int db = -1);
/// <summary>
/// Checks if Redis is connected and available.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Gets the Redis connection string being used.
/// </summary>
string? ConnectionString { get; }
}

View File

@@ -510,9 +510,56 @@ public static class ApiBootstrap
services.AddTransient<ICacheService, CacheService>();
services.AddSingleton<ITaskCache, TaskCache>();
// Redis (for SignalR backplane and other distributed scenarios)
services.AddRedis(configuration);
return services;
}
private static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
{
// Check if Redis is configured
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
var redisConnectionString = configuration.GetConnectionString("Redis")
?? configuration["REDIS_URL"];
if (!string.IsNullOrWhiteSpace(redisConnectionString))
{
Console.WriteLine($"✅ Redis configured: {MaskRedisPassword(redisConnectionString)}");
// Register generic Redis connection service for various use cases
// (SignalR backplane, distributed caching, pub/sub, etc.)
services.AddSingleton<IRedisConnectionService, RedisConnectionService>();
}
else
{
Console.WriteLine(" Redis not configured - running in single-instance mode");
// Register a no-op Redis service that returns null connections
services.AddSingleton<IRedisConnectionService>(sp =>
new RedisConnectionService(configuration, sp.GetRequiredService<ILogger<RedisConnectionService>>()));
}
return services;
}
private static string MaskRedisPassword(string connectionString)
{
if (connectionString.Contains("password=", StringComparison.OrdinalIgnoreCase))
{
var parts = connectionString.Split(',');
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].Trim().StartsWith("password=", StringComparison.OrdinalIgnoreCase))
{
parts[i] = "password=***";
}
}
return string.Join(",", parts);
}
return connectionString;
}
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetValue<bool>("WorkerNotifyBundleBacktest", false))

View File

@@ -9,6 +9,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
<PackageReference Include="System.Runtime.Caching" Version="8.0.0"/>
</ItemGroup>

View File

@@ -0,0 +1,329 @@
# Redis Integration Guide
## Overview
The Managing platform now includes a generic Redis integration that supports multiple use cases:
1. **SignalR Backplane** - Enables multi-instance SignalR hubs (BotHub, BacktestHub, LlmHub)
2. **Distributed Caching** - Can be extended for distributed caching scenarios
3. **Other Redis Use Cases** - Generic connection service available for any Redis operations
## Architecture
### Components
- **`IRedisConnectionService`** - Interface for Redis connectivity (in `Managing.Application.Abstractions`)
- **`RedisConnectionService`** - Implementation managing Redis connections (in `Managing.Infrastructure.Storage`)
- **SignalR Backplane** - Automatically configured when Redis is available (in `Managing.Api/Program.cs`)
### Design Principles
1. **Generic and Reusable** - The Redis service is in the Infrastructure layer and can be used by any part of the application
2. **Graceful Degradation** - If Redis is not configured, the application runs normally in single-instance mode
3. **Automatic Configuration** - SignalR automatically uses Redis backplane when available
4. **Connection Management** - Single connection multiplexer shared across the application
## Configuration
### Environment Variables
Redis can be configured using any of these methods (in order of precedence):
1. **ConnectionStrings:Redis** (recommended) - Set via `ConnectionStrings__Redis` environment variable
2. **REDIS_URL** (fallback environment variable)
### Configuration Examples
#### appsettings.json
```json
{
"ConnectionStrings": {
"Redis": "srv-captain--redis:6379"
}
}
```
#### With Password
```json
{
"ConnectionStrings": {
"Redis": "srv-captain--redis:6379,password=your-password-here"
}
}
```
#### Environment Variable (Recommended for CapRover/Docker)
**.NET Standard Format:**
```bash
export ConnectionStrings__Redis="srv-captain--redis:6379"
# or with password
export ConnectionStrings__Redis="srv-captain--redis:6379,password=your-password-here"
```
**Fallback Format:**
```bash
export REDIS_URL="srv-captain--redis:6379"
# or with password
export REDIS_URL="srv-captain--redis:6379,password=your-password-here"
```
### Current Configuration
- **Sandbox** (`appsettings.Sandbox.json`): `srv-captain--redis:6379`
- **Production** (`appsettings.Production.json`): `srv-captain--redis:6379`
- **Local Development** (`appsettings.json`): Not configured (single-instance mode)
## Deployment
### CapRover Requirements
For multi-instance SignalR to work properly on CapRover:
1. **Enable WebSocket Support**
- Go to App Settings
- Enable "WebSocket Support"
- Save and redeploy
2. **Enable Sticky Sessions**
- Go to HTTP Settings
- Enable "Sticky Sessions"
- Save
3. **Setup Redis**
- Create a Redis app in CapRover (or use external Redis)
- Note the service name (e.g., `srv-captain--redis`)
- Configure connection string in appsettings
4. **Set Instance Count**
- With Redis and sticky sessions, you can safely scale to multiple instances
- Recommended: 2-3 instances for high availability
### Docker/Docker Compose
If using Docker Compose, add a Redis service:
```yaml
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
redis-data:
```
Then set the connection string to `redis:6379`.
## SignalR Backplane
### How It Works
When Redis is configured:
1. All SignalR connections register in Redis
2. Messages sent to a hub are distributed via Redis pub/sub
3. Each instance receives messages for its connected clients
4. Connection IDs are shared across instances
### Benefits
- **Scale Out**: Run multiple API instances
- **High Availability**: If one instance dies, others continue working
- **Load Balancing**: Distribute WebSocket connections across instances
- **No "Connection ID Not Found" Errors**: All instances know about all connections
### Verification
Check the application logs on startup:
```
✅ Configuring SignalR with Redis backplane: srv-captain--redis:6379
✅ Redis connection established successfully
```
If Redis is not configured:
```
Redis not configured - SignalR running in single-instance mode
```
If Redis connection fails:
```
⚠️ Failed to configure SignalR Redis backplane: <error message>
SignalR will work in single-instance mode only
```
## Using Redis in Your Code
### Accessing Redis Connection
```csharp
public class MyService
{
private readonly IRedisConnectionService _redisService;
public MyService(IRedisConnectionService redisService)
{
_redisService = redisService;
}
public async Task<string?> GetValueAsync(string key)
{
if (!_redisService.IsConnected)
{
// Redis not available, handle gracefully
return null;
}
var db = _redisService.GetDatabase();
return await db.StringGetAsync(key);
}
public async Task SetValueAsync(string key, string value, TimeSpan? expiry = null)
{
if (!_redisService.IsConnected)
{
return;
}
var db = _redisService.GetDatabase();
await db.StringSetAsync(key, value, expiry);
}
}
```
### Advanced Usage
```csharp
// Get the connection multiplexer for advanced scenarios
var connection = _redisService.GetConnection();
if (connection != null)
{
// Pub/Sub
var subscriber = connection.GetSubscriber();
await subscriber.SubscribeAsync("my-channel", (channel, message) =>
{
Console.WriteLine($"Received: {message}");
});
// Multiple databases
var db0 = _redisService.GetDatabase(0);
var db1 = _redisService.GetDatabase(1);
// Server operations
var server = connection.GetServer(connection.GetEndPoints().First());
var keys = server.Keys();
}
```
## Monitoring
### Health Checks
The Redis connection service automatically:
- Reconnects on connection loss
- Logs connection events
- Provides connection status via `IsConnected` property
### Logging
Key log messages to watch:
- `✅ Redis connection established successfully` - Initial connection succeeded
- `✅ Redis connection restored` - Reconnected after failure
- `❌ Redis connection failed: <error>` - Connection problem
- `⚠️ Redis connection lost, attempting to reconnect...` - Transient failure
### CapRover Monitoring
In CapRover App Logs, search for:
- "Redis" - All Redis-related messages
- "SignalR" - SignalR configuration
- "No Connection with that ID" - SignalR connection issues (should not appear with Redis)
## Troubleshooting
### SignalR "No Connection with that ID" Error
**Symptoms**: Frontend gets 404 with "No Connection with that ID" after negotiation
**Causes**:
1. Multiple instances without Redis backplane
2. Sticky sessions not enabled
3. Redis not configured correctly
**Solutions**:
1. Enable Redis (see Configuration section)
2. Enable sticky sessions in CapRover
3. Verify Redis connection in logs
4. Temporarily set instances to 1 to test
### Redis Connection Failures
**Check**:
1. Redis service is running in CapRover
2. Connection string is correct (service name, port)
3. No firewall blocking the connection
4. Redis service is healthy (check its logs)
**Test Connection**:
```bash
# In CapRover terminal or SSH
redis-cli -h srv-captain--redis ping
# Should return: PONG
```
### Single Instance Still Shows Errors
If running single instance and getting SignalR errors:
1. Check WebSocket support is enabled in CapRover
2. Verify no proxy/load balancer issues
3. Check browser console for actual WebSocket error
4. Review CapRover Nginx configuration
## Performance
### Connection Pooling
The Redis connection service uses a single `ConnectionMultiplexer` instance shared across the application. This is the recommended approach for StackExchange.Redis.
### SignalR Message Volume
Redis pub/sub is used for SignalR messages:
- Low overhead for typical SignalR usage
- Efficient binary protocol
- Automatic message routing
### Scaling Considerations
- **2-3 instances**: Optimal for most workloads
- **4+ instances**: Consider Redis clustering for high availability
- Monitor Redis memory usage if using for caching
## Future Enhancements
The generic Redis service can be extended for:
1. **Distributed Caching**: Replace `IDistributedCache` implementation
2. **Session Storage**: Store user sessions in Redis
3. **Rate Limiting**: Use Redis for distributed rate limiting
4. **Pub/Sub**: Implement event-driven architecture
5. **Job Queues**: Background job processing with Redis
## References
- [StackExchange.Redis Documentation](https://stackexchange.github.io/StackExchange.Redis/)
- [SignalR Scale-out with Redis](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale)
- [Redis Best Practices](https://redis.io/docs/manual/patterns/)

View File

@@ -0,0 +1,160 @@
using Managing.Application.Abstractions.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Managing.Infrastructure.Storage;
/// <summary>
/// Generic Redis connection service that manages a single Redis connection multiplexer
/// to be shared across the application for various purposes (SignalR, caching, etc.)
/// </summary>
public class RedisConnectionService : IRedisConnectionService, IDisposable
{
private readonly ILogger<RedisConnectionService> _logger;
private readonly string? _connectionString;
private IConnectionMultiplexer? _connection;
private readonly object _lock = new object();
private bool _disposed;
public RedisConnectionService(IConfiguration configuration, ILogger<RedisConnectionService> logger)
{
_logger = logger;
// Try configuration keys in priority order
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
_connectionString = configuration.GetConnectionString("Redis")
?? configuration["REDIS_URL"];
if (string.IsNullOrWhiteSpace(_connectionString))
{
_logger.LogWarning("Redis connection string not configured. Redis features will be unavailable.");
return;
}
InitializeConnection();
}
private void InitializeConnection()
{
if (string.IsNullOrWhiteSpace(_connectionString))
{
return;
}
try
{
_logger.LogInformation("Initializing Redis connection to: {ConnectionString}",
MaskConnectionString(_connectionString));
var options = ConfigurationOptions.Parse(_connectionString);
// Configure connection options
options.AbortOnConnectFail = false; // Don't fail the app if Redis is down
options.ConnectTimeout = 5000; // 5 second timeout
options.SyncTimeout = 5000;
options.AsyncTimeout = 5000;
options.ConnectRetry = 3;
options.KeepAlive = 60; // Send keepalive every 60 seconds
_connection = ConnectionMultiplexer.Connect(options);
_connection.ConnectionFailed += OnConnectionFailed;
_connection.ConnectionRestored += OnConnectionRestored;
_connection.ErrorMessage += OnErrorMessage;
_logger.LogInformation("✅ Redis connection established successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to Redis. Redis features will be unavailable.");
_connection = null;
}
}
private void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e)
{
_logger.LogError("❌ Redis connection failed: {Exception}", e.Exception?.Message ?? "Unknown error");
}
private void OnConnectionRestored(object? sender, ConnectionFailedEventArgs e)
{
_logger.LogInformation("✅ Redis connection restored");
}
private void OnErrorMessage(object? sender, RedisErrorEventArgs e)
{
_logger.LogError("Redis error: {Message}", e.Message);
}
public IConnectionMultiplexer? GetConnection()
{
if (_connection != null && _connection.IsConnected)
{
return _connection;
}
// Try to reconnect if connection was lost
lock (_lock)
{
if (_connection == null || !_connection.IsConnected)
{
_logger.LogWarning("Redis connection lost, attempting to reconnect...");
_connection?.Dispose();
_connection = null;
InitializeConnection();
}
}
return _connection;
}
public IDatabase? GetDatabase(int db = -1)
{
var connection = GetConnection();
return connection?.GetDatabase(db);
}
public bool IsConnected => _connection != null && _connection.IsConnected;
public string? ConnectionString => _connectionString;
private static string MaskConnectionString(string connectionString)
{
// Mask password in connection string for logging
if (connectionString.Contains("password=", StringComparison.OrdinalIgnoreCase))
{
var parts = connectionString.Split(',');
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].Trim().StartsWith("password=", StringComparison.OrdinalIgnoreCase))
{
parts[i] = "password=***";
}
}
return string.Join(",", parts);
}
return connectionString;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_connection != null)
{
_logger.LogInformation("Disposing Redis connection");
_connection.ConnectionFailed -= OnConnectionFailed;
_connection.ConnectionRestored -= OnConnectionRestored;
_connection.ErrorMessage -= OnErrorMessage;
_connection.Dispose();
_connection = null;
}
}
}