< Summary

Information
Class: Chronicis.Client.Services.ArticleCacheService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/ArticleCacheService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 59
Coverable lines: 59
Total lines: 193
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 22
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetArticleInfoAsync()0%2040%
GetArticlePathAsync()0%620%
GetNavigationPathAsync()0%7280%
CacheArticle(...)0%4260%
InvalidateCache()100%210%
InvalidateArticle(...)0%620%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/ArticleCacheService.cs

#LineLine coverage
 1using Chronicis.Shared.DTOs;
 2
 3namespace Chronicis.Client.Services;
 4
 5/// <summary>
 6/// Cached article information for navigation and tooltips.
 7/// </summary>
 8public class CachedArticleInfo
 9{
 10    public Guid ArticleId { get; set; }
 11    public string Title { get; set; } = string.Empty;
 12    public string Slug { get; set; } = string.Empty;
 13    public string? DisplayPath { get; set; }
 14    public List<BreadcrumbDto>? Breadcrumbs { get; set; }
 15    public DateTime CachedAt { get; set; }
 16}
 17
 18/// <summary>
 19/// Service interface for caching article metadata to reduce API calls.
 20/// </summary>
 21public interface IArticleCacheService
 22{
 23    /// <summary>
 24    /// Gets cached article info, fetching from API if not cached.
 25    /// </summary>
 26    Task<CachedArticleInfo?> GetArticleInfoAsync(Guid articleId);
 27
 28    /// <summary>
 29    /// Gets the display path for an article (from cache if available).
 30    /// </summary>
 31    Task<string?> GetArticlePathAsync(Guid articleId);
 32
 33    /// <summary>
 34    /// Gets the navigation URL path for an article (from cache if available).
 35    /// </summary>
 36    Task<string?> GetNavigationPathAsync(Guid articleId);
 37
 38    /// <summary>
 39    /// Adds or updates an article in the cache (called when article is loaded).
 40    /// </summary>
 41    void CacheArticle(ArticleDto article);
 42
 43    /// <summary>
 44    /// Invalidates all cached data (called on save/delete).
 45    /// </summary>
 46    void InvalidateCache();
 47
 48    /// <summary>
 49    /// Invalidates a specific article from the cache.
 50    /// </summary>
 51    void InvalidateArticle(Guid articleId);
 52}
 53
 54/// <summary>
 55/// In-memory cache for article metadata to reduce API calls for tooltips and navigation.
 56/// </summary>
 57public class ArticleCacheService : IArticleCacheService
 58{
 059    private readonly Dictionary<Guid, CachedArticleInfo> _cache = new();
 60    private readonly IArticleApiService _articleApi;
 61    private readonly ILogger<ArticleCacheService> _logger;
 062    private readonly object _lock = new();
 63
 064    public ArticleCacheService(IArticleApiService articleApi, ILogger<ArticleCacheService> logger)
 65    {
 066        _articleApi = articleApi;
 067        _logger = logger;
 068    }
 69
 70    /// <summary>
 71    /// Gets cached article info, fetching from API if not cached.
 72    /// </summary>
 73    public async Task<CachedArticleInfo?> GetArticleInfoAsync(Guid articleId)
 74    {
 75        // Check cache first
 076        lock (_lock)
 77        {
 078            if (_cache.TryGetValue(articleId, out var cached))
 79            {
 080                _logger.LogDebug("Cache hit for article {ArticleId}", articleId);
 081                return cached;
 82            }
 083        }
 84
 85        // Fetch from API
 86        try
 87        {
 088            _logger.LogDebug("Cache miss for article {ArticleId}, fetching from API", articleId);
 089            var article = await _articleApi.GetArticleAsync(articleId);
 90
 091            if (article == null)
 92            {
 093                return null;
 94            }
 95
 96            // Cache the result
 097            CacheArticle(article);
 98
 099            lock (_lock)
 100            {
 0101                return _cache.GetValueOrDefault(articleId);
 102            }
 103        }
 0104        catch (Exception ex)
 105        {
 0106            _logger.LogError(ex, "Error fetching article {ArticleId} for cache", articleId);
 0107            return null;
 108        }
 0109    }
 110
 111    /// <summary>
 112    /// Gets the display path for an article (from cache if available).
 113    /// </summary>
 114    public async Task<string?> GetArticlePathAsync(Guid articleId)
 115    {
 0116        var info = await GetArticleInfoAsync(articleId);
 0117        return info?.DisplayPath;
 0118    }
 119
 120    /// <summary>
 121    /// Gets the navigation URL path for an article (from cache if available).
 122    /// </summary>
 123    public async Task<string?> GetNavigationPathAsync(Guid articleId)
 124    {
 0125        var info = await GetArticleInfoAsync(articleId);
 126
 0127        if (info?.Breadcrumbs != null && info.Breadcrumbs.Any())
 128        {
 0129            return string.Join("/", info.Breadcrumbs.Select(b => b.Slug));
 130        }
 131
 0132        return info?.Slug;
 0133    }
 134
 135    /// <summary>
 136    /// Adds or updates an article in the cache (called when article is loaded).
 137    /// </summary>
 138    public void CacheArticle(ArticleDto article)
 139    {
 0140        if (article == null)
 0141            return;
 142
 0143        var cachedInfo = new CachedArticleInfo
 0144        {
 0145            ArticleId = article.Id,
 0146            Title = article.Title,
 0147            Slug = article.Slug,
 0148            Breadcrumbs = article.Breadcrumbs,
 0149            CachedAt = DateTime.UtcNow
 0150        };
 151
 152        // Build display path from breadcrumbs, skipping the World name (first element)
 0153        if (article.Breadcrumbs != null && article.Breadcrumbs.Any())
 154        {
 0155            var pathSegments = article.Breadcrumbs.Skip(1).Select(b => b.Title);
 0156            cachedInfo.DisplayPath = string.Join(" / ", pathSegments);
 157        }
 158
 0159        lock (_lock)
 160        {
 0161            _cache[article.Id] = cachedInfo;
 0162        }
 163
 0164        _logger.LogDebug("Cached article {ArticleId}: {Title}", article.Id, article.Title);
 0165    }
 166
 167    /// <summary>
 168    /// Invalidates all cached data (called on save/delete).
 169    /// </summary>
 170    public void InvalidateCache()
 171    {
 0172        lock (_lock)
 173        {
 0174            var count = _cache.Count;
 0175            _cache.Clear();
 0176            _logger.LogDebug("Invalidated entire article cache ({Count} entries)", count);
 0177        }
 0178    }
 179
 180    /// <summary>
 181    /// Invalidates a specific article from the cache.
 182    /// </summary>
 183    public void InvalidateArticle(Guid articleId)
 184    {
 0185        lock (_lock)
 186        {
 0187            if (_cache.Remove(articleId))
 188            {
 0189                _logger.LogDebug("Invalidated cached article {ArticleId}", articleId);
 190            }
 0191        }
 0192    }
 193}