< Summary

Information
Class: Services.ApiServices._1CApi.Nomenclature.ProducerRegistry
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/Nomenclature/NomenclatureProviders.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 26
Coverable lines: 26
Total lines: 115
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 14
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(...)0%620%
GetAsync()0%156120%
Invalidate()100%210%

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>
 033public sealed class ProducerRegistry(IServiceScopeFactory scopeFactory, TimeSpan? ttl = null) : IProducerDirectoryProvid
 34{
 035    private readonly TimeSpan _ttl = ttl ?? TimeSpan.FromHours(1);
 036    private readonly SemaphoreSlim _gate = new(1, 1);
 37    private ProducerDirectory? _directory;
 038    private DateTime _expiresUtc = DateTime.MinValue;
 39
 40    public async Task<ProducerDirectory> GetAsync(CancellationToken cancellationToken = default)
 41    {
 042        var current = _directory;
 043        if (current != null && DateTime.UtcNow < _expiresUtc)
 044            return current;
 45
 046        await _gate.WaitAsync(cancellationToken);
 47        try
 48        {
 049            if (_directory != null && DateTime.UtcNow < _expiresUtc)
 050                return _directory;
 51
 052            await using var scope = scopeFactory.CreateAsyncScope();
 053            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 54
 055            var rows = await db.EtProducers.AsNoTracking()
 056                .Select(p => new ProducerRow(p.Id, p.RealId, p.Name, p.Prefix, p.ExistName, p.MarketPrefix))
 057                .ToListAsync(cancellationToken);
 058            var links = await db.OneCProducerAliases.AsNoTracking()
 059                .Where(a => a.CanonicalName != null)
 060                .Select(a => new ProducerLink(a.AliasNorm, a.CanonicalName!))
 061                .ToListAsync(cancellationToken);
 62
 063            _directory = ProducerDirectory.Build(rows, links);
 064            _expiresUtc = DateTime.UtcNow + _ttl;
 065            return _directory;
 066        }
 67        finally
 68        {
 069            _gate.Release();
 70        }
 071    }
 72
 073    public void Invalidate() => _expiresUtc = DateTime.MinValue;
 74}
 75
 76/// <summary>
 77/// Индекс карты соответствий в памяти (десятки МБ на ~100 тыс. строк) с кэшем на 10 минут. Снимок подменяется
 78/// целиком, поэтому читающие потоки видят либо старый, либо новый индекс, но не половину.
 79/// </summary>
 80public sealed class NomenclatureIndexProvider(
 81    INomenclatureSnapshotSource source,
 82    IProducerDirectoryProvider directoryProvider,
 83    TimeSpan? ttl = null) : INomenclatureIndexProvider
 84{
 85    private readonly TimeSpan _ttl = ttl ?? TimeSpan.FromMinutes(10);
 86    private readonly SemaphoreSlim _gate = new(1, 1);
 87    private NomenclatureIndex? _index;
 88    private DateTime _expiresUtc = DateTime.MinValue;
 89
 90    public async Task<NomenclatureIndex> GetAsync(CancellationToken cancellationToken = default)
 91    {
 92        var current = _index;
 93        if (current != null && DateTime.UtcNow < _expiresUtc)
 94            return current;
 95
 96        await _gate.WaitAsync(cancellationToken);
 97        try
 98        {
 99            if (_index != null && DateTime.UtcNow < _expiresUtc)
 100                return _index;
 101
 102            var directory = await directoryProvider.GetAsync(cancellationToken);
 103            var rows = await source.LoadAllAsync(cancellationToken);
 104            _index = NomenclatureIndex.Build(rows, directory);
 105            _expiresUtc = DateTime.UtcNow + _ttl;
 106            return _index;
 107        }
 108        finally
 109        {
 110            _gate.Release();
 111        }
 112    }
 113
 114    public void Invalidate() => _expiresUtc = DateTime.MinValue;
 115}