< Summary

Information
Class: Services.ApiServices._1CApi.Nomenclature.NomenclatureIndexProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/Nomenclature/NomenclatureProviders.cs
Line coverage
95%
Covered lines: 20
Uncovered lines: 1
Coverable lines: 21
Total lines: 115
Line coverage: 95.2%
Branch coverage
83%
Covered branches: 10
Total branches: 12
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
GetAsync()90%101092.3%
Invalidate()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/Nomenclature/NomenclatureProviders.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.DependencyInjection;
 3using OzonRepositories.Context;
 4
 5namespace Services.ApiServices._1CApi.Nomenclature;
 6
 7/// <summary>Источник целого снимка карты соответствий (по строке на пару «артикул + производитель»).</summary>
 8public interface INomenclatureSnapshotSource
 9{
 10    Task<IReadOnlyList<MapRow>> LoadAllAsync(CancellationToken cancellationToken = default);
 11}
 12
 13public interface IProducerDirectoryProvider
 14{
 15    Task<ProducerDirectory> GetAsync(CancellationToken cancellationToken = default);
 16
 17    /// <summary>Сбросить кэш: ручная связь написаний появилась, справочник производителей обновился.</summary>
 18    void Invalidate();
 19}
 20
 21public interface INomenclatureIndexProvider
 22{
 23    Task<NomenclatureIndex> GetAsync(CancellationToken cancellationToken = default);
 24
 25    /// <summary>Сбросить кэш: фоновая сверка с 1С дописала карточки или создана новая.</summary>
 26    void Invalidate();
 27}
 28
 29/// <summary>
 30/// Справочник групп бренда из <c>et_producers</c> и ручных связей <c>OneCProducerAliases</c> с кэшем на час.
 31/// Читает БД через собственный scope — безопасно для Singleton.
 32/// </summary>
 33public sealed class ProducerRegistry(IServiceScopeFactory scopeFactory, TimeSpan? ttl = null) : IProducerDirectoryProvid
 34{
 35    private readonly TimeSpan _ttl = ttl ?? TimeSpan.FromHours(1);
 36    private readonly SemaphoreSlim _gate = new(1, 1);
 37    private ProducerDirectory? _directory;
 38    private DateTime _expiresUtc = DateTime.MinValue;
 39
 40    public async Task<ProducerDirectory> GetAsync(CancellationToken cancellationToken = default)
 41    {
 42        var current = _directory;
 43        if (current != null && DateTime.UtcNow < _expiresUtc)
 44            return current;
 45
 46        await _gate.WaitAsync(cancellationToken);
 47        try
 48        {
 49            if (_directory != null && DateTime.UtcNow < _expiresUtc)
 50                return _directory;
 51
 52            await using var scope = scopeFactory.CreateAsyncScope();
 53            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 54
 55            var rows = await db.EtProducers.AsNoTracking()
 56                .Select(p => new ProducerRow(p.Id, p.RealId, p.Name, p.Prefix, p.ExistName, p.MarketPrefix))
 57                .ToListAsync(cancellationToken);
 58            var links = await db.OneCProducerAliases.AsNoTracking()
 59                .Where(a => a.CanonicalName != null)
 60                .Select(a => new ProducerLink(a.AliasNorm, a.CanonicalName!))
 61                .ToListAsync(cancellationToken);
 62
 63            _directory = ProducerDirectory.Build(rows, links);
 64            _expiresUtc = DateTime.UtcNow + _ttl;
 65            return _directory;
 66        }
 67        finally
 68        {
 69            _gate.Release();
 70        }
 71    }
 72
 73    public void Invalidate() => _expiresUtc = DateTime.MinValue;
 74}
 75
 76/// <summary>
 77/// Индекс карты соответствий в памяти (десятки МБ на ~100 тыс. строк) с кэшем на 10 минут. Снимок подменяется
 78/// целиком, поэтому читающие потоки видят либо старый, либо новый индекс, но не половину.
 79/// </summary>
 180public sealed class NomenclatureIndexProvider(
 181    INomenclatureSnapshotSource source,
 182    IProducerDirectoryProvider directoryProvider,
 183    TimeSpan? ttl = null) : INomenclatureIndexProvider
 84{
 185    private readonly TimeSpan _ttl = ttl ?? TimeSpan.FromMinutes(10);
 186    private readonly SemaphoreSlim _gate = new(1, 1);
 87    private NomenclatureIndex? _index;
 188    private DateTime _expiresUtc = DateTime.MinValue;
 89
 90    public async Task<NomenclatureIndex> GetAsync(CancellationToken cancellationToken = default)
 91    {
 392        var current = _index;
 393        if (current != null && DateTime.UtcNow < _expiresUtc)
 194            return current;
 95
 296        await _gate.WaitAsync(cancellationToken);
 97        try
 98        {
 299            if (_index != null && DateTime.UtcNow < _expiresUtc)
 0100                return _index;
 101
 2102            var directory = await directoryProvider.GetAsync(cancellationToken);
 2103            var rows = await source.LoadAllAsync(cancellationToken);
 2104            _index = NomenclatureIndex.Build(rows, directory);
 2105            _expiresUtc = DateTime.UtcNow + _ttl;
 2106            return _index;
 107        }
 108        finally
 109        {
 2110            _gate.Release();
 111        }
 3112    }
 113
 1114    public void Invalidate() => _expiresUtc = DateTime.MinValue;
 115}