< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridChangesService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridChangesService.cs
Line coverage
18%
Covered lines: 9
Uncovered lines: 41
Coverable lines: 50
Total lines: 152
Line coverage: 18%
Branch coverage
27%
Covered branches: 6
Total branches: 22
Branch coverage: 27.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%210%
GetChangesAsync()0%272160%
SplitChanges(...)100%66100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridChangesService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonRepositories.Context;
 4
 5namespace OzonOrdersWeb.WebServices.Orders.Grid;
 6
 7/// <summary>Запрос дельты: что изменилось после <see cref="Since"/> среди загруженных клиентом строк.</summary>
 8public sealed class OrdersGridChangesRequest
 9{
 10    /// <summary>Серверное время прошлого ответа (<see cref="OrdersGridChangesResult.Now"/>).</summary>
 11    public DateTime? Since { get; set; }
 12
 13    /// <summary>Id строк, которые сейчас в кэше грида (≤ MaxKnownIds).</summary>
 14    public List<int>? KnownIds { get; set; } = [];
 15
 16    /// <summary>Максимальный известный клиенту Id — всё, что больше, считается новыми заказами.</summary>
 17    public int MaxKnownId { get; set; }
 18
 19    public bool IncludeArchived { get; set; }
 20}
 21
 22public sealed class OrdersGridChangesResult
 23{
 24    /// <summary>Серверное время «сейчас» — клиент передаст его как Since в следующий раз.</summary>
 25    public DateTime Now { get; set; }
 26
 27    /// <summary>Since слишком старый или клиент прислал слишком много строк — надо перезагрузить список целиком.</summa
 28    public bool FullRefresh { get; set; }
 29
 30    /// <summary>Изменённые заказы из числа известных клиенту — готовые строки грида.</summary>
 31    public List<OrderGridRowDto> Updated { get; set; } = [];
 32
 33    /// <summary>Известные клиенту заказы, которые изменились, но не поместились в лимит гидрации (клиент перезапросит б
 34    public List<int> UpdatedOverflowIds { get; set; } = [];
 35
 36    /// <summary>Заказов с Id больше MaxKnownId (новые).</summary>
 37    public int NewCount { get; set; }
 38
 39    /// <summary>Изменившихся заказов, которых нет среди известных клиенту (вне загруженных блоков).</summary>
 40    public int ChangedOutsideCount { get; set; }
 41}
 42
 43public interface IOrdersGridChangesService
 44{
 45    Task<OrdersGridChangesResult> GetChangesAsync(OrdersGridChangesRequest request, ClaimsPrincipal principal, Cancellat
 46}
 47
 48/// <summary>
 49/// Дельта для автообновления грида: один индексный запрос по <c>Orders.UpdatedAt</c> (проставляется
 50/// BEFORE-триггером на любой INSERT/UPDATE), разбиение изменённых Id на «известные клиенту» / «новые» /
 51/// «вне экрана» и гидрация только известных через тот же конвейер, что и блоки списка.
 52/// </summary>
 53public sealed class OrdersGridChangesService : IOrdersGridChangesService
 54{
 55    public const int MaxKnownIds = 1000;
 56    public const int MaxHydrate = 50;
 57    /// <summary>Больше изменений за опрос — дешевле перезагрузить список, чем разбирать дельту.</summary>
 58    public const int MaxChanges = 500;
 159    public static readonly TimeSpan MaxSinceAge = TimeSpan.FromMinutes(10);
 60
 61    private readonly ApplicationDbContext _context;
 62    private readonly IOrdersGridRowHydrator _hydrator;
 63    private readonly IOrdersGridScopedQuery _scoped;
 64
 065    public OrdersGridChangesService(ApplicationDbContext context, IOrdersGridRowHydrator hydrator, IOrdersGridScopedQuer
 66    {
 067        _context = context;
 068        _hydrator = hydrator;
 069        _scoped = scoped;
 070    }
 71
 72    public async Task<OrdersGridChangesResult> GetChangesAsync(
 73        OrdersGridChangesRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken)
 74    {
 75        // Время берём из БД: триггер пишет NOW(6) сервера MariaDB, а не веб-сервера.
 076        var now = await _context.Database
 077            .SqlQueryRaw<DateTime>("SELECT NOW(6) AS Value")
 078            .SingleAsync(cancellationToken);
 79
 080        var result = new OrdersGridChangesResult { Now = now };
 081        var knownIds = request.KnownIds ?? [];
 82
 083        if (request.Since is null || now - request.Since.Value > MaxSinceAge || knownIds.Count > MaxKnownIds)
 84        {
 085            result.FullRefresh = true;
 086            return result;
 87        }
 88
 089        var user = await _scoped.ResolveUserAsync(principal);
 090        if (user is null)
 91        {
 092            result.FullRefresh = true;
 093            return result;
 94        }
 95
 96        // Только доступные пользователю заказы (RLS, профиль, архив) и не больше MaxChanges + 1:
 97        // по индексу IX_Orders_UpdatedAt (InnoDB хранит в нём и Id — запрос покрывающий).
 098        var since = request.Since.Value;
 099        var scoped = await _scoped.BuildAsync(user, request.IncludeArchived, cancellationToken);
 0100        var changed = await scoped
 0101            .Where(o => o.UpdatedAt != null && o.UpdatedAt > since)
 0102            .OrderBy(o => o.UpdatedAt)
 0103            .Select(o => o.Id)
 0104            .Take(MaxChanges + 1)
 0105            .ToListAsync(cancellationToken);
 106
 0107        if (changed.Count == 0)
 108        {
 0109            return result;
 110        }
 111
 0112        if (changed.Count > MaxChanges)
 113        {
 0114            result.FullRefresh = true;
 0115            return result;
 116        }
 117
 0118        var known = new HashSet<int>(knownIds);
 0119        var split = SplitChanges(changed, known, request.MaxKnownId);
 0120        result.NewCount = split.NewIds.Count;
 0121        result.ChangedOutsideCount = split.OutsideIds.Count;
 122
 0123        if (split.KnownIds.Count == 0)
 124        {
 0125            return result;
 126        }
 127
 0128        var toHydrate = split.KnownIds.Take(MaxHydrate).ToList();
 0129        result.UpdatedOverflowIds = split.KnownIds.Skip(MaxHydrate).ToList();
 130
 0131        result.Updated = await _hydrator.HydrateAsync(toHydrate, principal, cancellationToken);
 132
 0133        return result;
 0134    }
 135
 136    /// <summary>Чистое разбиение изменённых Id: известные клиенту / новые (Id &gt; max) / прочие вне экрана.</summary>
 137    public static (List<int> KnownIds, List<int> NewIds, List<int> OutsideIds) SplitChanges(
 138        IReadOnlyList<int> changedIds, IReadOnlySet<int> knownIds, int maxKnownId)
 139    {
 2140        var known = new List<int>();
 2141        var fresh = new List<int>();
 2142        var outside = new List<int>();
 16143        foreach (var id in changedIds)
 144        {
 8145            if (knownIds.Contains(id)) known.Add(id);
 6146            else if (id > maxKnownId) fresh.Add(id);
 2147            else outside.Add(id);
 148        }
 149
 2150        return (known, fresh, outside);
 151    }
 152}