From 6f692ba8e55f04c98ed482f594de0725d278a29a Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Thu, 19 Feb 2026 17:48:24 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20UnitFilterService=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20cancellationToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitFilterService/IUnitFilterService.cs | 14 ++- .../UnitFilterService/UnitFilterService.cs | 86 +++++++++++-------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/PARR.DAL/DomainServices/UnitFilterService/IUnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/IUnitFilterService.cs index b3a50076..f7619047 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/IUnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/IUnitFilterService.cs @@ -23,7 +23,8 @@ namespace PARR.DAL.DomainServices.UnitFilterService /// Список ID Unit'ов Task?> GetUnitsIdByJobFilterAsync( Job job, - int? takeCount = null); + int? takeCount = null, + CancellationToken cancellationToken = default); /// /// Получает отфильтрованные Unit'ы с полной информацией о значениях полей и связях. @@ -34,7 +35,8 @@ namespace PARR.DAL.DomainServices.UnitFilterService /// Список UnitFilterResultDto или null, если Job не найден Task?> GetUnitsByJobFilterAsync( Guid jobId, - int? takeCount = null); + int? takeCount = null, + CancellationToken cancellationToken = default); /// /// Получает отфильтрованные Unit'ы с полной информацией о значениях полей и связях. @@ -45,7 +47,9 @@ namespace PARR.DAL.DomainServices.UnitFilterService /// Список UnitFilterResultDto Task?> GetUnitsByJobFilterAsync( Job job, - int? takeCount = null); + int? takeCount = null, + CancellationToken cancellationToken = default + ); /// /// Получает имена связанных Unit'ов для заданного Unit, отфильтрованных по RelationshipFilters Job. @@ -56,6 +60,8 @@ namespace PARR.DAL.DomainServices.UnitFilterService /// Список имён связанных Unit'ов (уникальные, без дублей) Task> GetRelatedUnitNamesAsync( Guid jobId, - Guid unitId); + Guid unitId, + CancellationToken cancellationToken = default + ); } } diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index e29ebca9..bab9577c 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -49,21 +49,33 @@ internal class UnitFilterService : IUnitFilterService } - public async Task?> GetUnitsByJobFilterAsync(Guid jobId, int? takeCount = null) + public async Task?> GetUnitsByJobFilterAsync( + Guid jobId, + int? takeCount = null, + CancellationToken cancellationToken = default + ) { var job = await LoadJobWithFiltersAsync(jobId); - return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount); + return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken); } - public async Task?> GetUnitsIdByJobFilterAsync(Job job, int? takeCount = null) + public async Task?> GetUnitsIdByJobFilterAsync( + Job job, + int? takeCount = null, + CancellationToken cancellationToken = default + ) { - var filteredUnits = await GetUnitsByJobFilterAsync(job, takeCount); + var filteredUnits = await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken); return filteredUnits?.Select(u => u.Id).ToList(); } - public async Task?> GetUnitsByJobFilterAsync(Job job, int? takeCount = null) + public async Task?> GetUnitsByJobFilterAsync( + Job job, + int? takeCount = null, + CancellationToken cancellationToken = default + ) { if (job.Group == null) throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group"); @@ -92,7 +104,7 @@ internal class UnitFilterService : IUnitFilterService logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id); // 1. Найти ID юнитов по UnitFilter (Name LIKE) - var initialUnitIds = await GetUnitIdsFromCacheOrDbAsync(filter); + var initialUnitIds = await GetUnitIdsFromCacheOrDbAsync(filter, cancellationToken); if (!initialUnitIds.Any()) { logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов)", i + 1); @@ -102,13 +114,13 @@ internal class UnitFilterService : IUnitFilterService #if DEBUG if (initialUnitIds.Contains(targetUnitId)) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", targetUnitId, i + 1); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", targetUnitId, i + 1); } #endif // 2. Применить FieldFilters на уровне SQL var fieldStopwatch = Stopwatch.StartNew(); - var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters); + var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters, cancellationToken); fieldStopwatch.Stop(); if (!fieldFilteredIds.Any()) @@ -120,13 +132,13 @@ internal class UnitFilterService : IUnitFilterService #if DEBUG if (initialUnitIds.Contains(targetUnitId) && !fieldFilteredIds.Contains(targetUnitId)) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", targetUnitId, i + 1); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", targetUnitId, i + 1); } #endif // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext var relStopwatch = Stopwatch.StartNew(); - var relationshipFilteredContexts = await ApplyRelationshipFiltersOnDbAsync(fieldFilteredIds, filter.RelationshipFilters); + var relationshipFilteredContexts = await ApplyRelationshipFiltersOnDbAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken); relStopwatch.Stop(); if (!relationshipFilteredContexts.Any()) @@ -139,7 +151,7 @@ internal class UnitFilterService : IUnitFilterService var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId); if (targetContext != null) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}", + logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}", targetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count); } #endif @@ -194,7 +206,7 @@ internal class UnitFilterService : IUnitFilterService : targetBeforeUmbrella.ValidChildIds.Count; var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue); - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", + logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", targetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes); } #endif @@ -207,7 +219,7 @@ internal class UnitFilterService : IUnitFilterService var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId); if (targetBeforeUmbrella != null && targetAfterUmbrella == null) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", targetUnitId); } #endif } @@ -252,7 +264,8 @@ internal class UnitFilterService : IUnitFilterService /// private async Task> ApplyFieldFiltersOnDbAsync( List unitIds, - IEnumerable fieldFilters) + IEnumerable fieldFilters, + CancellationToken cancellationToken = default) { if (!fieldFilters.Any()) return unitIds; @@ -303,7 +316,7 @@ internal class UnitFilterService : IUnitFilterService } // Выполняем промежуточный запрос для логирования - var intermediateResult = await query.Select(u => u.Id).ToListAsync(); + var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken); logger.LogDebug(" После фильтра #{Index}: осталось {Count} юнитов", filterIndex, intermediateResult.Count); // Обновляем query для следующей итерации @@ -311,7 +324,7 @@ internal class UnitFilterService : IUnitFilterService .Where(u => intermediateResult.Contains(u.Id)); } - var result = await query.Select(u => u.Id).ToListAsync(); + var result = await query.Select(u => u.Id).ToListAsync(cancellationToken); logger.LogDebug("ApplyFieldFiltersOnDbAsync: выход {UnitCount} юнитов", result.Count); return result; @@ -326,7 +339,8 @@ internal class UnitFilterService : IUnitFilterService /// private async Task> ApplyRelationshipFiltersOnDbAsync( List unitIds, - IEnumerable relationshipFilters + IEnumerable relationshipFilters, + CancellationToken cancellationToken = default ) { if (!relationshipFilters.Any() || !unitIds.Any()) @@ -342,7 +356,7 @@ internal class UnitFilterService : IUnitFilterService #if DEBUG if (unitIds.Contains(targetUnitId)) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} присутствует во входных данных ApplyRelationshipFiltersOnDbAsync", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} присутствует во входных данных ApplyRelationshipFiltersOnDbAsync", targetUnitId); } #endif @@ -360,7 +374,7 @@ internal class UnitFilterService : IUnitFilterService .AsNoTracking() .Where(link => unitIds.Contains(link.ChildUnitId)) .Select(link => new { ChildId = link.ChildUnitId, ParentId = link.ParentUnitId }) - .ToListAsync(); + .ToListAsync(cancellationToken); var allParentIds = allParentLinks.Select(l => l.ParentId).Distinct().ToList(); logger.LogDebug(" Найдено {ParentCount} уникальных родителей для {LinkCount} связей", @@ -398,7 +412,7 @@ internal class UnitFilterService : IUnitFilterService v.FieldId == relFilter.FieldId && EF.Functions.Like(v.Value.Value, dbValueMask))) .Select(u => u.Id) - .ToListAsync(); + .ToListAsync(cancellationToken); logger.LogDebug(" Найдено {MatchCount} родителей по маске", matchingParents.Count); @@ -439,7 +453,7 @@ internal class UnitFilterService : IUnitFilterService .AsNoTracking() .Where(link => unitIds.Contains(link.ParentUnitId)) .Select(link => new { ParentId = link.ParentUnitId, ChildId = link.ChildUnitId }) - .ToListAsync(); + .ToListAsync(cancellationToken); var allChildIds = allChildLinks.Select(l => l.ChildId).Distinct().ToList(); logger.LogDebug(" Найдено {ChildCount} уникальных детей для {LinkCount} связей", @@ -477,7 +491,7 @@ internal class UnitFilterService : IUnitFilterService v.FieldId == relFilter.FieldId && EF.Functions.Like(v.Value.Value, dbValueMask))) .Select(u => u.Id) - .ToListAsync(); + .ToListAsync(cancellationToken); logger.LogDebug(" Найдено {MatchCount} детей по маске", matchingChildren.Count); @@ -540,7 +554,7 @@ internal class UnitFilterService : IUnitFilterService #if DEBUG if (context.UnitId == targetUnitId) { - logger.LogDebug("🎯 DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", + logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", targetUnitId, count, min, max, passes); } #endif @@ -559,7 +573,7 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private async Task> LoadFinalResultAsync(List contexts) + private async Task> LoadFinalResultAsync(List contexts, CancellationToken cancellationToken = default) { var allResults = new List(); @@ -567,19 +581,19 @@ internal class UnitFilterService : IUnitFilterService var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == targetUnitId); if (targetInContexts != null) { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}", + logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}", targetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count); } else { - logger.LogDebug("🎯 DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", targetUnitId); } #endif for (int i = 0; i < contexts.Count; i += batchSize) { var batch = contexts.Skip(i).Take(batchSize).ToList(); - var batchResults = await LoadBatchAsync(batch); + var batchResults = await LoadBatchAsync(batch, cancellationToken); allResults.AddRange(batchResults); } @@ -592,7 +606,7 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private async Task> LoadBatchAsync(List batch) + private async Task> LoadBatchAsync(List batch, CancellationToken cancellationToken = default) { var unitIds = batch.Select(c => c.UnitId).ToList(); var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList(); @@ -603,7 +617,7 @@ internal class UnitFilterService : IUnitFilterService .AsSingleQuery() .Include(u => u.UnitValues).ThenInclude(v => v.Value) .Where(u => unitIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id); + .ToDictionaryAsync(u => u.Id, cancellationToken); // Загружаем родителей Dictionary parentsMap; @@ -613,7 +627,7 @@ internal class UnitFilterService : IUnitFilterService .AsSingleQuery() .Include(u => u.UnitValues).ThenInclude(v => v.Value) .Where(u => allParentIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id); + .ToDictionaryAsync(u => u.Id, cancellationToken); } else { @@ -628,7 +642,7 @@ internal class UnitFilterService : IUnitFilterService .AsSingleQuery() .Include(u => u.UnitValues).ThenInclude(v => v.Value) .Where(u => allChildIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id); + .ToDictionaryAsync(u => u.Id, cancellationToken); } else { @@ -681,14 +695,14 @@ internal class UnitFilterService : IUnitFilterService - public async Task> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId) + public async Task> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default) { logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId); var job = await jobService .Get().AsNoTracking() .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters) - .FirstOrDefaultAsync(j => j.Id == jobId); + .FirstOrDefaultAsync(j => j.Id == jobId, cancellationToken); if (job == null) { @@ -762,7 +776,7 @@ internal class UnitFilterService : IUnitFilterService var names = await unitService.Get().AsNoTracking() .Where(u => matchingUnitIds.Contains(u.Id)) .Select(u => u.Name) - .ToListAsync(); + .ToListAsync(cancellationToken); result.UnionWith(names); } } @@ -772,7 +786,7 @@ internal class UnitFilterService : IUnitFilterService #region вспомогательные методы - private async Task> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter) + private async Task> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default) { var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true); @@ -785,7 +799,7 @@ internal class UnitFilterService : IUnitFilterService var initialUnitIds = await unitService.Get().AsNoTracking() .Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter)) .Select(u => u.Id) - .ToListAsync(); + .ToListAsync(cancellationToken); var toCache = new UnitFilterIds {