feat: add email-api-data project with EmailTemplates repository and service
New data project owning the emailApi schema: - EmailTemplateEntity with Key, Language, Value, Description, UpdatedAt, OperatorCopy - EmailApiDbContext (schema: emailApi, custom migration table _EmailApiMigrations) - IEmailTemplateRepository / EfEmailTemplateRepository (scoped) - IEmailTemplateService / EmailTemplateService (singleton, 10-min cache) - GetOperatorCopy falls back to first non-empty OperatorCopy across all rows - Initial migration CreateEmailTemplates: creates table + seeds all email.* templates (match + search-results in en/ro) and html-shell fragments with OperatorCopy = "contact@myai.ro" for addressable rows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Concurrent;
|
||||
using EmailApi.Data.Repositories.Contracts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace EmailApi.Data.Services;
|
||||
|
||||
public sealed class EmailTemplateService : IEmailTemplateService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<EmailTemplateService> _logger;
|
||||
private ConcurrentDictionary<string, string> _valueCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private ConcurrentDictionary<string, string> _operatorCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private DateTime _loadedAt = DateTime.MinValue;
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
||||
|
||||
public EmailTemplateService(IServiceScopeFactory scopeFactory, ILogger<EmailTemplateService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string Get(string key, string language = "en")
|
||||
{
|
||||
EnsureCacheLoaded();
|
||||
|
||||
if (_valueCache.TryGetValue(CacheKey(key, language), out var value))
|
||||
return value;
|
||||
|
||||
if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)
|
||||
&& _valueCache.TryGetValue(CacheKey(key, "en"), out var fallback))
|
||||
return fallback;
|
||||
|
||||
_logger.LogWarning("Email 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;
|
||||
}
|
||||
|
||||
public string? GetOperatorCopy(string key, string language)
|
||||
{
|
||||
EnsureCacheLoaded();
|
||||
|
||||
if (_operatorCache.TryGetValue(CacheKey(key, language), out var specific)
|
||||
&& !string.IsNullOrWhiteSpace(specific))
|
||||
return specific;
|
||||
|
||||
// Fall back to first non-empty OperatorCopy in the cache
|
||||
foreach (var val in _operatorCache.Values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
return val;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void EnsureCacheLoaded()
|
||||
{
|
||||
if (DateTime.UtcNow - _loadedAt < CacheTtl) return;
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IEmailTemplateRepository>();
|
||||
var rows = repo.GetAllAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
var freshValues = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var freshOperator = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
freshValues[CacheKey(row.Key, row.Language)] = row.Value;
|
||||
freshOperator[CacheKey(row.Key, row.Language)] = row.OperatorCopy;
|
||||
}
|
||||
|
||||
_valueCache = freshValues;
|
||||
_operatorCache = freshOperator;
|
||||
_loadedAt = DateTime.UtcNow;
|
||||
_logger.LogDebug("Email template cache refreshed. {Count} templates loaded.", rows.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh email template cache. Serving stale cache.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string CacheKey(string key, string language) => $"{key}::{language}";
|
||||
}
|
||||
Reference in New Issue
Block a user