| using Microsoft.EntityFrameworkCore; |
| using Microsoft.Extensions.Caching.Distributed; |
| using Microsoft.Extensions.Logging; |
| using OptimAI.BRE.RuleEngine.Domain; |
| using OptimAI.BRE.Shared.Domain; |
| using System.Text.Json; |
|
|
| namespace OptimAI.BRE.RuleEngine.Infrastructure; |
|
|
| |
| |
| |
| |
| public sealed class CachedRuleLoader : IRuleLoader |
| { |
| private readonly BREDbContext _db; |
| private readonly IDistributedCache _cache; |
| private readonly ILogger<CachedRuleLoader> _logger; |
|
|
| private static readonly JsonSerializerOptions _jsonOpts = new() |
| { |
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
| }; |
|
|
| public CachedRuleLoader(BREDbContext db, IDistributedCache cache, ILogger<CachedRuleLoader> logger) |
| { |
| _db = db; |
| _cache = cache; |
| _logger = logger; |
| } |
|
|
| public async Task<IReadOnlyList<Rule>> LoadRulesAsync(RuleLoadRequest request, CancellationToken ct = default) |
| { |
| var cacheKey = BuildCacheKey(request); |
|
|
| var cached = await _cache.GetStringAsync(cacheKey, ct); |
| if (cached != null) |
| { |
| _logger.LogDebug("Cache HIT for rules: {Key}", cacheKey); |
| return JsonSerializer.Deserialize<List<Rule>>(cached, _jsonOpts) ?? new(); |
| } |
|
|
| _logger.LogDebug("Cache MISS for rules: {Key}", cacheKey); |
| var rules = await LoadFromDatabaseAsync(request, ct); |
|
|
| await _cache.SetStringAsync(cacheKey, |
| JsonSerializer.Serialize(rules, _jsonOpts), |
| new DistributedCacheEntryOptions |
| { |
| AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) |
| }, ct); |
|
|
| return rules; |
| } |
|
|
| public async Task<Rule?> LoadRuleByCodeAsync(Guid tenantId, string ruleCode, CancellationToken ct = default) |
| { |
| return await _db.Rules |
| .Include(r => r.CurrentVersion) |
| .Include(r => r.Scopes) |
| .FirstOrDefaultAsync(r => r.TenantId == tenantId && r.RuleCode == ruleCode, ct); |
| } |
|
|
| private async Task<List<Rule>> LoadFromDatabaseAsync(RuleLoadRequest request, CancellationToken ct) |
| { |
| var query = _db.Rules |
| .Include(r => r.CurrentVersion) |
| .Include(r => r.Scopes) |
| .Where(r => r.TenantId == request.TenantId); |
|
|
| if (request.PublishedOnly) |
| query = query.Where(r => r.IsPublished && r.Status == RuleStatus.Published); |
|
|
| if (request.RuleTypes.Any()) |
| query = query.Where(r => request.RuleTypes.Contains(r.RuleType)); |
|
|
| if (request.RuleSetId.HasValue) |
| { |
| var ruleIds = await _db.RuleSetMembers |
| .Where(m => m.SetId == request.RuleSetId.Value) |
| .Select(m => m.RuleId) |
| .ToListAsync(ct); |
|
|
| query = query.Where(r => ruleIds.Contains(r.Id)); |
| } |
| else |
| { |
| |
| query = query.Where(r => |
| !r.Scopes.Any() || |
| r.Scopes.Any(s => s.ScopeType == ScopeType.Global) || |
| (request.ProductCode != null && r.Scopes.Any(s => |
| s.ScopeType == ScopeType.Product && s.ScopeValue == request.ProductCode && !s.IsExcluded)) || |
| (request.BranchCode != null && r.Scopes.Any(s => |
| s.ScopeType == ScopeType.Branch && s.ScopeValue == request.BranchCode && !s.IsExcluded)) || |
| (request.StageCode != null && r.Scopes.Any(s => |
| s.ScopeType == ScopeType.Stage && s.ScopeValue == request.StageCode && !s.IsExcluded)) |
| ); |
| } |
|
|
| return await query |
| .OrderBy(r => r.Priority) |
| .AsNoTracking() |
| .ToListAsync(ct); |
| } |
|
|
| public async Task InvalidateCacheAsync(Guid tenantId, CancellationToken ct = default) |
| { |
| |
| |
| var pattern = $"bre:rules:{tenantId}:*"; |
| _logger.LogInformation("Invalidating rule cache for tenant {TenantId}", tenantId); |
| |
| } |
|
|
| private static string BuildCacheKey(RuleLoadRequest request) |
| { |
| var parts = new[] |
| { |
| "bre", "rules", |
| request.TenantId.ToString(), |
| request.ProductCode ?? "ALL", |
| request.BranchCode ?? "ALL", |
| request.StageCode ?? "ALL", |
| request.RuleSetId?.ToString() ?? "ALL", |
| string.Join(",", request.RuleTypes.OrderBy(t => t.ToString())), |
| request.PublishedOnly ? "pub" : "all" |
| }; |
| return string.Join(":", parts); |
| } |
| } |
|
|