< Summary

Information
Class: OzonOrdersWeb.WebServices.Sidebar.UserSidebarSettingsService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Sidebar/UserSidebarSettingsService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 46
Coverable lines: 46
Total lines: 115
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 24
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
CacheKey(...)100%210%
GetAllAsync()0%2040%
SaveAsync()0%420200%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Sidebar/UserSidebarSettingsService.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Caching.Memory;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5
 6namespace OzonOrdersWeb.WebServices.Sidebar;
 7
 8public interface IUserSidebarSettingsService
 9{
 10    /// <summary>Все настройки пользователя: key → JSON. Кешируется в памяти: сайдбар рендерится на каждой странице.</su
 11    Task<IReadOnlyDictionary<string, string>> GetAllAsync(string userId, CancellationToken cancellationToken);
 12
 13    /// <summary>
 14    /// Сохранить значение; <paramref name="version"/> — версия клиента (монотонно растущая на ключ).
 15    /// Пустое значение удаляет настройку. Возвращает false, если в базе уже более новая версия.
 16    /// </summary>
 17    Task<bool> SaveAsync(string userId, string key, string? json, long version, CancellationToken cancellationToken);
 18}
 19
 20/// <summary>
 21/// Настройки бокового меню по пользователю в <c>UserSidebarSettings</c>. Логика хранения повторяет
 22/// <c>OrdersGridUserSettingsService</c> (JSON по ключу, версия клиента, надгробия вместо удаления);
 23/// сверху — memory-кеш на пользователя, потому что меню читается при каждом рендере страницы.
 24/// </summary>
 25public sealed class UserSidebarSettingsService : IUserSidebarSettingsService
 26{
 027    public static readonly IReadOnlySet<string> AllowedKeys = new HashSet<string>(StringComparer.Ordinal)
 028    {
 029        "favorites", "order", "collapsed", "prefs",
 030        SidebarNavigationCatalog.AdminKeyPrefix + "favorites", SidebarNavigationCatalog.AdminKeyPrefix + "order",
 031        SidebarNavigationCatalog.AdminKeyPrefix + "collapsed", SidebarNavigationCatalog.AdminKeyPrefix + "prefs"
 032    };
 33
 34    private const int MaxValueLength = 64 * 1024;
 035    private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
 36
 37    private readonly ApplicationDbContext _context;
 38    private readonly IMemoryCache _cache;
 39
 040    public UserSidebarSettingsService(ApplicationDbContext context, IMemoryCache cache)
 41    {
 042        _context = context;
 043        _cache = cache;
 044    }
 45
 046    private static string CacheKey(string userId) => "sidebar-settings:" + userId;
 47
 48    public async Task<IReadOnlyDictionary<string, string>> GetAllAsync(string userId, CancellationToken cancellationToke
 49    {
 050        if (_cache.TryGetValue(CacheKey(userId), out IReadOnlyDictionary<string, string>? cached) && cached is not null)
 51        {
 052            return cached;
 53        }
 54
 55        // Пустое Value — «надгробие» удалённой настройки (строка хранит Version, чтобы отклонять опоздавшие записи).
 056        var loaded = await _context.UserSidebarSettings
 057            .AsNoTracking()
 058            .Where(s => s.UserId == userId && s.Value != string.Empty)
 059            .ToDictionaryAsync(s => s.Key, s => s.Value, cancellationToken);
 60
 061        _cache.Set(CacheKey(userId), (IReadOnlyDictionary<string, string>)loaded, CacheTtl);
 062        return loaded;
 063    }
 64
 65    public async Task<bool> SaveAsync(string userId, string key, string? json, long version, CancellationToken cancellat
 66    {
 067        if (!AllowedKeys.Contains(key))
 68        {
 069            throw new ArgumentException($"Неизвестный ключ настройки: {key}", nameof(key));
 70        }
 71
 072        if (json is { Length: > MaxValueLength })
 73        {
 074            throw new ArgumentException("Слишком большое значение настройки.", nameof(json));
 75        }
 76
 077        var existing = await _context.UserSidebarSettings
 078            .FirstOrDefaultAsync(s => s.UserId == userId && s.Key == key, cancellationToken);
 79
 80        // Optimistic concurrency по версии клиента: запрос со старой версией пришёл позже нового — не затираем.
 081        if (existing != null && version > 0 && version < existing.Version)
 82        {
 083            return false;
 84        }
 85
 086        var now = DateTime.UtcNow;
 087        if (string.IsNullOrWhiteSpace(json))
 88        {
 089            if (existing != null)
 90            {
 091                existing.Value = string.Empty;
 092                existing.UpdatedAt = now;
 093                existing.Version = Math.Max(version, existing.Version);
 94            }
 095            else if (version > 0)
 96            {
 097                _context.UserSidebarSettings.Add(new UserSidebarSetting { UserId = userId, Key = key, Value = string.Emp
 98            }
 99        }
 0100        else if (existing == null)
 101        {
 0102            _context.UserSidebarSettings.Add(new UserSidebarSetting { UserId = userId, Key = key, Value = json, UpdatedA
 103        }
 104        else
 105        {
 0106            existing.Value = json;
 0107            existing.UpdatedAt = now;
 0108            existing.Version = Math.Max(version, existing.Version);
 109        }
 110
 0111        await _context.SaveChangesAsync(cancellationToken);
 0112        _cache.Remove(CacheKey(userId));
 0113        return true;
 0114    }
 115}