Files
Deal/src/core/Deal.Api/Services/AuditReferenceResolver.cs
T

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;
}
}
}
}