Files
myAi/Apis/myai-models/Services/DbTemplateService.cs
T
claude fc6fe7a78b feat: DB-backed localized templates + language-aware emails
- New Apis/myai-models project: MyAiDbContext (schema myAi), TemplateEntity,
  ITemplateService, DbTemplateService with 10-min in-memory cache
- Seeds EN+RO variants for all user-facing templates (match email, job search
  results email, HTML status pages, AI system prompt)
- Match result email now sent in user's UI language (en/ro)
- Job search results email now respects session language
- Language propagates: MatchJobRequest -> token -> session -> email
- Add Language column to JobSearchTokens and JobSearchSessions (default 'en')
- All three Dockerfiles updated to include myai-models in build context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:06:44 +03:00

71 lines
2.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MyAi.Models.Data;
using System.Collections.Concurrent;
namespace MyAi.Models.Services;
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;
}
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;
}
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;
}
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.");
}
}
private static string CacheKey(string key, string language) => $"{key}::{language}";
}