16bb195cb5
- Update CLAUDE.md: replace incorrect 'no XML doc on internal code' rule with the correct convention (XML doc on all public methods and non-trivial private/protected helpers) - Restore /// <summary> on FileDownloadController private helpers (HandleRangeRequest, StreamRangeAsync) - Add full XML doc to all service contracts: ICaptchaVerifier, IEmailSender, ICvMatcherService, IJobTextExtractor, IJobTokenService, IDocumentClassifier, IRagService, ITextChunker, ITextExtractor, IEmailTemplateService, ITemplateService - Add /// <summary> and /// <inheritdoc /> to all concrete service classes and their methods: RecaptchaVerifier, EmailApiEmailSender, SmtpEmailDispatcher, CvMatcherService, JobTextExtractor, JobTokenService, RagService, DocumentClassifier, TextChunker, TextExtractor, HtmlJobSearcher, CvSearchEmailSender, CvSearchJobTask, EmailTemplateService, DbTemplateService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
3.1 KiB
C#
83 lines
3.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using MyAi.Data;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace MyAi.Data.Services;
|
|
|
|
/// <summary>
|
|
/// Singleton implementation of <see cref="ITemplateService"/> that caches all templates from the
|
|
/// <c>myAi.Templates</c> table and refreshes them every 10 minutes.
|
|
/// Uses <see cref="IServiceScopeFactory"/> to resolve the scoped DbContext from a singleton lifetime.
|
|
/// </summary>
|
|
public sealed class DbTemplateService : ITemplateService
|
|
{
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ILogger<DbTemplateService> _logger;
|
|
private ConcurrentDictionary<string, string> _cache = new(StringComparer.OrdinalIgnoreCase);
|
|
private DateTime _loadedAt = DateTime.MinValue;
|
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
|
|
|
public DbTemplateService(IServiceScopeFactory scopeFactory, ILogger<DbTemplateService> logger)
|
|
{
|
|
_scopeFactory = scopeFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Get(string key, string language = "en")
|
|
{
|
|
EnsureCacheLoaded();
|
|
|
|
if (_cache.TryGetValue(CacheKey(key, language), out var value))
|
|
return value;
|
|
|
|
if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)
|
|
&& _cache.TryGetValue(CacheKey(key, "en"), out var fallback))
|
|
return fallback;
|
|
|
|
_logger.LogWarning("Template not found: key={Key}, language={Language}", key, language);
|
|
return key;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Render(string key, string language, params (string Key, string Value)[] placeholders)
|
|
{
|
|
var template = Get(key, language);
|
|
foreach (var (k, v) in placeholders)
|
|
template = template.Replace($"{{{{{k}}}}}", v, StringComparison.OrdinalIgnoreCase);
|
|
return template;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads all templates from the database when the cache TTL has expired.
|
|
/// Swaps the cache atomically; logs an error and continues serving the stale cache on failure.
|
|
/// </summary>
|
|
private void EnsureCacheLoaded()
|
|
{
|
|
if (DateTime.UtcNow - _loadedAt < CacheTtl) return;
|
|
|
|
try
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
|
var rows = db.Templates.AsNoTracking().ToList();
|
|
var fresh = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var row in rows)
|
|
fresh[CacheKey(row.Key, row.Language)] = row.Value;
|
|
|
|
_cache = fresh;
|
|
_loadedAt = DateTime.UtcNow;
|
|
_logger.LogDebug("Template cache refreshed. {Count} templates loaded.", rows.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to refresh template cache. Serving stale cache.");
|
|
}
|
|
}
|
|
|
|
/// <summary>Builds the dictionary key used in the cache.</summary>
|
|
private static string CacheKey(string key, string language) => $"{key}::{language}";
|
|
}
|