< Summary

Information
Class: OzonRepositories.Data.EtProducerRepository
Assembly: OzonRepositories
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonRepositories/Data/EtProducerRepository.cs
Line coverage
6%
Covered lines: 10
Uncovered lines: 146
Coverable lines: 156
Total lines: 309
Line coverage: 6.4%
Branch coverage
0%
Covered branches: 0
Total branches: 42
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Id()100%210%
get_Rows()100%210%
get_LoadedAtUtc()100%210%
.cctor()100%210%
.ctor(...)100%11100%
Add()100%210%
Delete()100%210%
Get()100%210%
GetByRealId(...)100%210%
GetQueryable()100%11100%
GetAsync()100%210%
Get(...)100%210%
GetAsync()100%210%
GetRealIdAsyncByCode()100%210%
GetAllByCodeAsync()100%11100%
GetRealIdAsyncByName()0%620%
Update()100%210%
GetAllMarketPrefixesAsync()100%210%
GetMarketPrefixesByNamesAsync()0%110100%
GetAliasesByPrefixAsync()0%4260%
LoadManufacturerRowsAsync()100%210%
LoadProducerPrefixRowsAsync()0%156120%
IsTransientDbError(...)0%156120%
NormalizeProducerName(...)100%210%
SaveChanges()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonRepositories/Data/EtProducerRepository.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using MySqlConnector;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5
 6namespace OzonRepositories.Data;
 7
 8public class EtProducerRepository : IRepository<EtProducer>
 9{
 010    private sealed record ProducerPrefixRow(int Id, int RealId, string Prefix, string? MarketPrefix);
 11
 12    private sealed class ProducerPrefixCacheEntry
 13    {
 014        public List<ProducerPrefixRow> Rows { get; init; } = new();
 015        public DateTime LoadedAtUtc { get; init; }
 16    }
 17
 018    private static readonly object ProducerCacheLock = new();
 19    private static ProducerPrefixCacheEntry? _producerCache;
 020    private static readonly TimeSpan ProducerCacheTtl = TimeSpan.FromMinutes(30);
 21
 22    ApplicationDbContext _context;
 23
 3424    public EtProducerRepository(ApplicationDbContext context)
 25    {
 3426        _context = context;
 3427    }
 28
 29    public async Task<int> Add(EtProducer value)
 30    {
 031        await _context.EtProducers.AddAsync(value);
 032        return await _context.SaveChangesAsync();
 033    }
 34
 35    public async Task<int> Delete(EtProducer value)
 36    {
 037        _context.EtProducers.Remove(value);
 038        return await _context.SaveChangesAsync();
 039    }
 40
 41    public async Task<List<EtProducer>> Get()
 42    {
 043        return await _context.EtProducers.AsNoTracking().ToListAsync();
 044    }
 45
 46    public IQueryable<EtProducer> GetByRealId(int? realId)
 47    {
 048        return _context.EtProducers.AsNoTracking().Where(e => e.RealId == realId);
 49    }
 50
 51    public IQueryable<EtProducer> GetQueryable()
 52    {
 1753        return _context.EtProducers.AsNoTracking().AsQueryable();
 54    }
 55
 56    public async Task<EtProducer?> GetAsync(int id)
 57    {
 058        return await _context.EtProducers
 059            .AsNoTracking()
 060            .FirstOrDefaultAsync(e => e.Id == id);
 061    }
 62
 63    public EtProducer? Get(int id)
 64    {
 065        return _context.EtProducers
 066            .AsNoTracking()
 067            .FirstOrDefault(e => e.Id == id);
 68    }
 69
 70
 71    public async Task<EtProducer?> GetAsync(EtProducer value)
 72    {
 073        return await _context.EtProducers.AsNoTracking()
 074            .FirstOrDefaultAsync(e => e.MarketPrefix == value.MarketPrefix);
 075    }
 76
 77    public async Task<EtProducer?> GetRealIdAsyncByCode(string marketPrefix)
 78    {
 079        return await _context.EtProducers
 080            .AsNoTracking()
 081            .Where(r => r.MarketPrefix == marketPrefix || r.Prefix == marketPrefix)
 082            .OrderBy(r => r.Id)
 083            .FirstOrDefaultAsync();
 084    }
 85
 86    /// <summary>
 87    /// Все производители с данным префиксом: один MarketPrefix бывает общим у разных брендов
 88    /// (HND — и Honda, и Hyundai).
 89    /// </summary>
 90    public async Task<List<EtProducer>> GetAllByCodeAsync(string marketPrefix)
 91    {
 292        return await _context.EtProducers
 293            .AsNoTracking()
 294            .Where(r => r.MarketPrefix == marketPrefix || r.Prefix == marketPrefix)
 295            .OrderBy(r => r.Id)
 296            .ToListAsync();
 297    }
 98
 99
 100    public async Task<EtProducer?> GetRealIdAsyncByName(string name)
 101    {
 0102        if (string.IsNullOrEmpty(name))
 0103            return null;
 104
 105        // Получаем все и фильтруем в памяти
 0106        var allProducers = await _context.EtProducers
 0107            .Where(p => p.Name != null)
 0108            .AsNoTracking()
 0109            .ToListAsync();
 110
 0111        return allProducers.FirstOrDefault(p =>
 0112            p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
 0113    }
 114
 115    public async Task<int> Update(EtProducer value)
 116    {
 0117        _context.EtProducers.Attach(value);
 0118        _context.Entry(value).State = EntityState.Modified;
 119
 0120        _context.Entry(value).Property(x => x.Name).IsModified = true;
 0121        _context.Entry(value).Property(x => x.MarketPrefix).IsModified = true;
 122
 0123        return await _context.SaveChangesAsync();
 0124    }
 125
 126    /// <summary>Все непустые MarketPrefix из кэша EtProducers (cached).</summary>
 127    public async Task<HashSet<string>> GetAllMarketPrefixesAsync()
 128    {
 0129        var rows = await LoadProducerPrefixRowsAsync();
 0130        return rows
 0131            .Where(r => !string.IsNullOrWhiteSpace(r.MarketPrefix))
 0132            .Select(r => r.MarketPrefix!)
 0133            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 0134    }
 135
 136    public async Task<Dictionary<string, string?>> GetMarketPrefixesByNamesAsync(List<string> names)
 137    {
 0138        if (names == null || names.Count == 0)
 0139            return new Dictionary<string, string?>();
 140
 141        List<ProducerPrefixRow> allProducers;
 142        try
 143        {
 0144            allProducers = await LoadProducerPrefixRowsAsync();
 0145        }
 0146        catch
 147        {
 0148            return names.Distinct().ToDictionary(n => n, _ => (string?)null);
 149        }
 150
 0151        var producersByNormalizedPrefix = allProducers
 0152            .ToLookup(p => p.Prefix.Replace(" ", "").ToLower());
 153
 0154        var manufacturerRows = await LoadManufacturerRowsAsync();
 0155        var resolverProducers = allProducers
 0156            .Select(p => new EtProducerMarketPrefixResolver.ProducerRow(p.Id, p.RealId, p.Prefix, p.MarketPrefix))
 0157            .ToList();
 158
 0159        var result = new Dictionary<string, string?>();
 160
 0161        foreach (var originalName in names)
 162        {
 0163            if (string.IsNullOrEmpty(originalName))
 164            {
 0165                result[originalName] = null;
 0166                continue;
 167            }
 168
 0169            var normalized = originalName.Replace(" ", "").ToLower();
 0170            var matches = producersByNormalizedPrefix[normalized].ToList();
 171
 0172            string? marketPrefix = null;
 173
 0174            if (matches.Count > 0)
 175            {
 0176                var match = matches[0];
 0177                marketPrefix = EtProducerMarketPrefixResolver.Resolve(
 0178                    new EtProducerMarketPrefixResolver.ProducerRow(match.Id, match.RealId, match.Prefix, match.MarketPre
 0179                    resolverProducers,
 0180                    manufacturerRows);
 181            }
 182
 0183            result[originalName] = marketPrefix;
 184        }
 185
 0186        return result;
 0187    }
 188
 189    /// <summary>
 190    /// Возвращает все алиасы производителя из той же группы <c>realId</c>, что и переданный Prefix.
 191    /// Нормализация намеренно убирает пробелы, кавычки и регистр: в ET встречаются варианты
 192    /// вроде LYNX, LYNXauto и "LYNX AUTO ".
 193    /// </summary>
 194    public async Task<IReadOnlyList<string>> GetAliasesByPrefixAsync(string prefix)
 195    {
 0196        if (string.IsNullOrWhiteSpace(prefix))
 0197            return Array.Empty<string>();
 198
 0199        var normalizedPrefix = NormalizeProducerName(prefix);
 0200        if (normalizedPrefix.Length == 0)
 0201            return Array.Empty<string>();
 202
 0203        var rows = await LoadProducerPrefixRowsAsync();
 0204        var match = rows.FirstOrDefault(row =>
 0205            string.Equals(NormalizeProducerName(row.Prefix), normalizedPrefix, StringComparison.Ordinal));
 0206        if (match is null)
 0207            return Array.Empty<string>();
 208
 0209        return rows
 0210            .Where(row => row.RealId == match.RealId)
 0211            .Select(row => row.Prefix)
 0212            .Where(value => !string.IsNullOrWhiteSpace(value))
 0213            .Distinct(StringComparer.OrdinalIgnoreCase)
 0214            .ToArray();
 0215    }
 216
 217    private async Task<List<EtProducerMarketPrefixResolver.ManufacturerRow>> LoadManufacturerRowsAsync()
 218    {
 219        try
 220        {
 0221            return await _context.Manufacturers
 0222                .AsNoTracking()
 0223                .Where(m => m.Code != null && m.Code != "")
 0224                .Select(m => new EtProducerMarketPrefixResolver.ManufacturerRow(
 0225                    m.EtProduserId,
 0226                    m.Code!.Trim(),
 0227                    m.Name))
 0228                .ToListAsync();
 229        }
 0230        catch
 231        {
 0232            return new List<EtProducerMarketPrefixResolver.ManufacturerRow>();
 233        }
 0234    }
 235
 236    private async Task<List<ProducerPrefixRow>> LoadProducerPrefixRowsAsync()
 237    {
 0238        lock (ProducerCacheLock)
 239        {
 0240            if (_producerCache != null && DateTime.UtcNow - _producerCache.LoadedAtUtc < ProducerCacheTtl)
 241            {
 0242                return _producerCache.Rows;
 243            }
 0244        }
 245
 246        const int maxAttempts = 3;
 0247        Exception? last = null;
 0248        for (var attempt = 1; attempt <= maxAttempts; attempt++)
 249        {
 250            try
 251            {
 0252                var rows = await _context.EtProducers
 0253                    .Where(p => p.Prefix != null)
 0254                    .Select(p => new ProducerPrefixRow(p.Id, p.RealId, p.Prefix!, p.MarketPrefix))
 0255                    .AsNoTracking()
 0256                    .ToListAsync();
 257
 0258                lock (ProducerCacheLock)
 259                {
 0260                    _producerCache = new ProducerPrefixCacheEntry
 0261                    {
 0262                        Rows = rows,
 0263                        LoadedAtUtc = DateTime.UtcNow
 0264                    };
 0265                }
 266
 0267                return rows;
 268            }
 0269            catch (Exception ex) when (IsTransientDbError(ex) && attempt < maxAttempts)
 270            {
 0271                last = ex;
 0272                await Task.Delay(TimeSpan.FromMilliseconds(300 * attempt));
 273            }
 274        }
 275
 0276        lock (ProducerCacheLock)
 277        {
 0278            if (_producerCache?.Rows.Count > 0)
 279            {
 0280                return _producerCache.Rows;
 281            }
 0282        }
 283
 0284        throw last ?? new InvalidOperationException("Не удалось загрузить EtProducers");
 0285    }
 286
 287    private static bool IsTransientDbError(Exception ex)
 288    {
 0289        for (var current = ex; current != null; current = current.InnerException)
 290        {
 0291            if (current is MySqlException or MySqlEndOfStreamException or TimeoutException or IOException)
 292            {
 0293                return true;
 294            }
 295        }
 296
 0297        return false;
 298    }
 299
 0300    private static string NormalizeProducerName(string value) => new(value
 0301        .Where(char.IsLetterOrDigit)
 0302        .Select(char.ToUpperInvariant)
 0303        .ToArray());
 304
 305    public async Task<int> SaveChanges()
 306    {
 0307        return await _context.SaveChangesAsync();
 0308    }
 309}