# Redis Caching - Detailed Reference

Detailed code examples cho Redis caching patterns trong GoodGo.

## Table of Contents

1. [Connection Setup](#connection-setup)
2. [Cache Service](#cache-service)
3. [Shopping Cart](#shopping-cart)
4. [Rate Limiting](#rate-limiting)
5. [Distributed Locks](#distributed-locks)
6. [Session Management](#session-management)

---

## Connection Setup

### Redis Configuration

```csharp
/// <summary>
/// EN: Configure Redis for distributed cache and direct access.
/// VI: Cấu hình Redis cho distributed cache và truy cập trực tiếp.
/// </summary>

// appsettings.json
{
  "Redis": {
    "ConnectionString": "localhost:6379,abortConnect=false,connectTimeout=5000",
    "InstanceName": "GoodGo:",
    "DefaultDatabase": 0
  }
}

// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = builder.Configuration["Redis:InstanceName"];
});

// EN: For advanced scenarios (Lua scripts, pub/sub, etc.)
// VI: Cho các trường hợp nâng cao (Lua scripts, pub/sub, v.v.)
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var config = ConfigurationOptions.Parse(
        builder.Configuration["Redis:ConnectionString"]!);
    config.AbortOnConnectFail = false;
    config.ConnectRetry = 3;
    return ConnectionMultiplexer.Connect(config);
});
```

---

## Cache Service

### Complete Cache Service Implementation

```csharp
/// <summary>
/// EN: Full-featured cache service with serialization and error handling.
/// VI: Cache service đầy đủ tính năng với serialization và xử lý lỗi.
/// </summary>
public class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<RedisCacheService> _logger;
    private readonly JsonSerializerOptions _jsonOptions;

    public RedisCacheService(
        IDistributedCache cache,
        IConnectionMultiplexer redis,
        ILogger<RedisCacheService> logger)
    {
        _cache = cache;
        _redis = redis;
        _logger = logger;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
    {
        try
        {
            var data = await _cache.GetStringAsync(key, ct);
            if (string.IsNullOrEmpty(data))
            {
                _logger.LogDebug("Cache MISS: {Key}", key);
                return default;
            }

            _logger.LogDebug("Cache HIT: {Key}", key);
            return JsonSerializer.Deserialize<T>(data, _jsonOptions);
        }
        catch (RedisConnectionException ex)
        {
            _logger.LogWarning(ex, "Redis connection failed for key: {Key}", key);
            return default; // EN: Graceful degradation / VI: Xử lý graceful
        }
    }

    public async Task SetAsync<T>(
        string key,
        T value,
        TimeSpan? absoluteExpiry = null,
        TimeSpan? slidingExpiry = null,
        CancellationToken ct = default)
    {
        try
        {
            var options = new DistributedCacheEntryOptions();
            
            if (absoluteExpiry.HasValue)
                options.AbsoluteExpirationRelativeToNow = absoluteExpiry;
            if (slidingExpiry.HasValue)
                options.SlidingExpiration = slidingExpiry;

            var data = JsonSerializer.Serialize(value, _jsonOptions);
            await _cache.SetStringAsync(key, data, options, ct);

            _logger.LogDebug("Cache SET: {Key}", key);
        }
        catch (RedisConnectionException ex)
        {
            _logger.LogWarning(ex, "Failed to cache key: {Key}", key);
            // EN: Don't throw - cache is not critical / VI: Không throw - cache không critical
        }
    }

    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<CancellationToken, Task<T>> factory,
        TimeSpan? expiry = null,
        CancellationToken ct = default)
    {
        var cached = await GetAsync<T>(key, ct);
        if (cached != null)
            return cached;

        // EN: Use lock to prevent cache stampede
        // VI: Dùng lock để tránh cache stampede
        var lockKey = $"lock:{key}";
        var db = _redis.GetDatabase();

        if (await db.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10)))
        {
            try
            {
                // EN: Double-check after acquiring lock
                // VI: Kiểm tra lại sau khi lấy lock
                cached = await GetAsync<T>(key, ct);
                if (cached != null)
                    return cached;

                var value = await factory(ct);
                await SetAsync(key, value, expiry, ct: ct);
                return value;
            }
            finally
            {
                await db.LockReleaseAsync(lockKey, Environment.MachineName);
            }
        }

        // EN: Lock not acquired, wait and retry
        // VI: Không lấy được lock, chờ và thử lại
        await Task.Delay(100, ct);
        return await GetOrSetAsync(key, factory, expiry, ct);
    }

    public async Task RemoveAsync(string key, CancellationToken ct = default)
    {
        await _cache.RemoveAsync(key, ct);
        _logger.LogDebug("Cache REMOVE: {Key}", key);
    }

    public async Task RemoveByPatternAsync(string pattern, CancellationToken ct = default)
    {
        var server = _redis.GetServer(_redis.GetEndPoints().First());
        var keys = server.Keys(pattern: pattern).ToArray();

        if (keys.Length > 0)
        {
            var db = _redis.GetDatabase();
            await db.KeyDeleteAsync(keys);
            _logger.LogDebug("Cache REMOVE pattern: {Pattern}, Count: {Count}", pattern, keys.Length);
        }
    }
}
```

---

## Shopping Cart

### Cart Entity

```csharp
/// <summary>
/// EN: Shopping cart stored in Redis.
/// VI: Giỏ hàng lưu trong Redis.
/// </summary>
public class Cart
{
    public string Id { get; set; } = default!;
    public string UserId { get; set; } = default!;
    public List<CartItem> Items { get; set; } = new();
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

    public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);

    public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice)
    {
        var existing = Items.FirstOrDefault(i => i.ProductId == productId);
        if (existing != null)
        {
            existing.Quantity += quantity;
            existing.UnitPrice = unitPrice; // EN: Update price
        }
        else
        {
            Items.Add(new CartItem
            {
                ProductId = productId,
                ProductName = productName,
                Quantity = quantity,
                UnitPrice = unitPrice
            });
        }
        UpdatedAt = DateTime.UtcNow;
    }

    public void RemoveItem(Guid productId)
    {
        Items.RemoveAll(i => i.ProductId == productId);
        UpdatedAt = DateTime.UtcNow;
    }
}

public class CartItem
{
    public Guid ProductId { get; set; }
    public string ProductName { get; set; } = default!;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}
```

### Cart Repository

```csharp
/// <summary>
/// EN: Redis-backed cart repository.
/// VI: Repository giỏ hàng với Redis.
/// </summary>
public class RedisCartRepository : ICartRepository
{
    private readonly IDatabase _db;
    private readonly ILogger<RedisCartRepository> _logger;
    private static readonly TimeSpan DefaultExpiry = TimeSpan.FromDays(7);

    public RedisCartRepository(
        IConnectionMultiplexer redis,
        ILogger<RedisCartRepository> logger)
    {
        _db = redis.GetDatabase();
        _logger = logger;
    }

    public async Task<Cart?> GetByUserIdAsync(string userId, CancellationToken ct = default)
    {
        var key = GetKey(userId);
        var data = await _db.StringGetAsync(key);

        if (data.IsNullOrEmpty)
            return null;

        return JsonSerializer.Deserialize<Cart>(data!);
    }

    public async Task SaveAsync(Cart cart, CancellationToken ct = default)
    {
        var key = GetKey(cart.UserId);
        var data = JsonSerializer.Serialize(cart);

        await _db.StringSetAsync(key, data, DefaultExpiry);
        
        _logger.LogInformation(
            "Cart saved for user {UserId}, Items: {ItemCount}",
            cart.UserId, cart.Items.Count);
    }

    public async Task DeleteAsync(string userId, CancellationToken ct = default)
    {
        var key = GetKey(userId);
        await _db.KeyDeleteAsync(key);
        
        _logger.LogInformation("Cart deleted for user {UserId}", userId);
    }

    private static string GetKey(string userId) => $"cart:{userId}";
}
```

---

## Rate Limiting

### Redis Rate Limiter

```csharp
/// <summary>
/// EN: Redis-based sliding window rate limiter.
/// VI: Rate limiter sliding window với Redis.
/// </summary>
public class RedisRateLimiter : IRateLimiter
{
    private readonly IDatabase _db;
    private readonly ILogger<RedisRateLimiter> _logger;

    public RedisRateLimiter(
        IConnectionMultiplexer redis,
        ILogger<RedisRateLimiter> logger)
    {
        _db = redis.GetDatabase();
        _logger = logger;
    }

    public async Task<RateLimitResult> CheckAsync(
        string key,
        int maxRequests,
        TimeSpan window)
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        var windowStart = now - (long)window.TotalMilliseconds;
        var redisKey = $"ratelimit:{key}";

        // EN: Lua script for atomic sliding window
        // VI: Lua script cho sliding window atomic
        var script = @"
            redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
            local count = redis.call('ZCARD', KEYS[1])
            if count < tonumber(ARGV[2]) then
                redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
                redis.call('EXPIRE', KEYS[1], ARGV[4])
                return {1, count + 1, tonumber(ARGV[2])}
            else
                local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
                local retryAfter = oldest[2] + tonumber(ARGV[5]) - tonumber(ARGV[3])
                return {0, count, tonumber(ARGV[2]), retryAfter}
            end
        ";

        var result = await _db.ScriptEvaluateAsync(script,
            new RedisKey[] { redisKey },
            new RedisValue[]
            {
                windowStart,
                maxRequests,
                now,
                (int)window.TotalSeconds,
                (long)window.TotalMilliseconds
            });

        var values = (RedisResult[])result!;
        var allowed = (int)values[0] == 1;
        var currentCount = (int)values[1];
        var limit = (int)values[2];
        var retryAfter = values.Length > 3 ? TimeSpan.FromMilliseconds((long)values[3]) : TimeSpan.Zero;

        if (!allowed)
        {
            _logger.LogWarning(
                "Rate limit exceeded for {Key}: {Count}/{Limit}",
                key, currentCount, limit);
        }

        return new RateLimitResult(allowed, currentCount, limit, retryAfter);
    }
}

public record RateLimitResult(
    bool IsAllowed,
    int CurrentCount,
    int Limit,
    TimeSpan RetryAfter);
```

---

## Distributed Locks

### Redis Lock Service

```csharp
/// <summary>
/// EN: Distributed lock using Redis.
/// VI: Distributed lock dùng Redis.
/// </summary>
public class RedisLockService : IDistributedLockService
{
    private readonly IDatabase _db;
    private readonly ILogger<RedisLockService> _logger;

    public async Task<IAsyncDisposable?> AcquireAsync(
        string resource,
        TimeSpan expiry,
        TimeSpan? waitTime = null,
        CancellationToken ct = default)
    {
        var lockKey = $"lock:{resource}";
        var lockValue = Guid.NewGuid().ToString();
        var deadline = DateTime.UtcNow + (waitTime ?? TimeSpan.Zero);

        do
        {
            if (await _db.LockTakeAsync(lockKey, lockValue, expiry))
            {
                _logger.LogDebug("Lock acquired: {Resource}", resource);
                return new RedisLock(_db, lockKey, lockValue, _logger);
            }

            if (waitTime.HasValue)
                await Task.Delay(50, ct);

        } while (waitTime.HasValue && DateTime.UtcNow < deadline && !ct.IsCancellationRequested);

        _logger.LogWarning("Failed to acquire lock: {Resource}", resource);
        return null;
    }

    private class RedisLock : IAsyncDisposable
    {
        private readonly IDatabase _db;
        private readonly string _key;
        private readonly string _value;
        private readonly ILogger _logger;

        public RedisLock(IDatabase db, string key, string value, ILogger logger)
        {
            _db = db;
            _key = key;
            _value = value;
            _logger = logger;
        }

        public async ValueTask DisposeAsync()
        {
            await _db.LockReleaseAsync(_key, _value);
            _logger.LogDebug("Lock released: {Key}", _key);
        }
    }
}

// EN: Usage / VI: Cách dùng
await using var lockHandle = await _lockService.AcquireAsync(
    $"order:{orderId}",
    TimeSpan.FromMinutes(1),
    waitTime: TimeSpan.FromSeconds(5));

if (lockHandle == null)
    throw new ConflictException("Order is being processed");

await ProcessOrderAsync(orderId);
```

---

## Resources / Tài Nguyên

- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/)
- [Microsoft Distributed Caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)
- [Redis Documentation](https://redis.io/documentation)
