134 lines
4.4 KiB
C#
134 lines
4.4 KiB
C#
using Deal.Modules.Cards.Application.Models;
|
|
using Deal.Modules.Kanban.Application.Abstractions;
|
|
using Deal.Modules.Kanban.Application.Models;
|
|
using Deal.Modules.Tenants.Application.Models;
|
|
using Deal.SharedKernel.Tenants.Abstractions;
|
|
using Deal.SharedKernel.Tenants.Models;
|
|
|
|
namespace Deal.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Резолвер ссылок аудита на данные тенанта
|
|
/// </summary>
|
|
/// <param name="scopeFactory">Фабрика scope для чтения схемы тенанта.</param>
|
|
/// <param name="logger">Логгер сбоев разрешения ссылок.</param>
|
|
public sealed class AuditReferenceResolver(
|
|
IServiceScopeFactory scopeFactory,
|
|
ILogger<AuditReferenceResolver> logger) : IAuditReferenceResolver
|
|
{
|
|
async Task<IReadOnlyDictionary<string, string>> IAuditReferenceResolver.ResolveAsync(
|
|
IReadOnlyList<AuditRecordDto> records,
|
|
CancellationToken ct)
|
|
{
|
|
var names = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
foreach (IGrouping<Guid, AuditRecordDto> group in GroupByTenant(records))
|
|
{
|
|
await ResolveTenantAsync(group.Key, group, names, ct);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
private static IEnumerable<IGrouping<Guid, AuditRecordDto>> GroupByTenant(IReadOnlyList<AuditRecordDto> records) =>
|
|
records
|
|
.Where(record => record.TenantId is not null)
|
|
.GroupBy(record => record.TenantId!.Value);
|
|
|
|
private async Task ResolveTenantAsync(
|
|
Guid tenantId,
|
|
IEnumerable<AuditRecordDto> records,
|
|
Dictionary<string, string> names,
|
|
CancellationToken ct)
|
|
{
|
|
var cardIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var boardIds = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (AuditRecordDto record in records)
|
|
{
|
|
foreach (AuditChangeDto change in record.AuditChanges())
|
|
{
|
|
Collect(change.From, cardIds, boardIds);
|
|
Collect(change.To, cardIds, boardIds);
|
|
}
|
|
}
|
|
|
|
if (cardIds.Count == 0 && boardIds.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
|
|
ITenantContext tenantContext = scope.ServiceProvider.GetRequiredService<ITenantContext>();
|
|
try
|
|
{
|
|
tenantContext.SetTenant(new TenantId(tenantId.ToString("N")));
|
|
ICardStore store = scope.ServiceProvider.GetRequiredService<ICardStore>();
|
|
await ResolveCardsAsync(store, cardIds, names, ct);
|
|
await ResolveContainersAsync(store, boardIds, names, ct);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogWarning(exception, "Резолвер аудита: ссылки тенанта {TenantId} не разрешены", tenantId);
|
|
}
|
|
finally
|
|
{
|
|
tenantContext.Reset();
|
|
}
|
|
}
|
|
|
|
private static void Collect(
|
|
string? value,
|
|
HashSet<string> cardIds,
|
|
HashSet<string> boardIds)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value.StartsWith(CardIds.CardPrefix, StringComparison.Ordinal))
|
|
{
|
|
cardIds.Add(value);
|
|
}
|
|
else if (value.StartsWith(KanbanIdPrefixes.Board, StringComparison.Ordinal))
|
|
{
|
|
boardIds.Add(value);
|
|
}
|
|
}
|
|
|
|
private static async Task ResolveCardsAsync(
|
|
ICardStore store,
|
|
HashSet<string> cardIds,
|
|
Dictionary<string, string> names,
|
|
CancellationToken ct)
|
|
{
|
|
foreach (string cardId in cardIds)
|
|
{
|
|
CardDto? card = await store.GetCardAsync(cardId, ct);
|
|
if (card is not null && !string.IsNullOrWhiteSpace(card.Title))
|
|
{
|
|
names[cardId] = card.Title;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task ResolveContainersAsync(
|
|
ICardStore store,
|
|
HashSet<string> boardIds,
|
|
Dictionary<string, string> names,
|
|
CancellationToken ct)
|
|
{
|
|
foreach (string boardId in boardIds)
|
|
{
|
|
ContainerDto? container = await store.GetContainerAsync(boardId, ct);
|
|
if (container is not null && !string.IsNullOrWhiteSpace(container.Name))
|
|
{
|
|
names[boardId] = container.Name;
|
|
}
|
|
}
|
|
}
|
|
}
|