Refactor error handling in Bumper, Media, Group, and Junction components to include contextual information
ci / build-backend (push) Successful in 1m35s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m46s
ci / sonar (push) Successful in 4m43s

Updated error definitions in BumperErrors, MediaErrors, GroupErrors, and TemplateErrors to accept a parameter for contextual information, enhancing the clarity of error messages. Refactored related command handlers to utilize these updated error messages, ensuring that users receive specific details about the usage context when attempting to delete resources. Adjusted unit tests to verify the correctness of the new error handling logic.
This commit is contained in:
Leonid Pershin
2026-07-28 13:19:30 +03:00
parent f895e1ddbc
commit f924891f3b
12 changed files with 227 additions and 27 deletions
@@ -34,10 +34,55 @@ public sealed class DeleteGroupCommandHandler(IAppDbContext dbContext)
t => t.FallbackGroupId == group.Id,
cancellationToken
);
// Сам факт занятости и адрес — разные вопросы: адрес собирается по цепочке слой → шаблон →
// канал, и если она неполна, имени не будет, а запрещать удаление всё равно надо.
if (used)
return Result.Failure(GroupErrors.InUse);
return Result.Failure(
GroupErrors.InUse(await UsedByAsync(group.Id, cancellationToken))
);
dbContext.Groups.Remove(group);
return Result.Success();
}
/// <summary>Кто держит группу: слоты каналов, стыки и аварийная группа шаблона.</summary>
private async Task<string> UsedByAsync(Guid groupId, CancellationToken cancellationToken)
{
var slots = await (
from slot in dbContext.Slots
join layer in dbContext.GridLayers on slot.LayerId equals layer.Id
join template in dbContext.ScheduleTemplates on layer.TemplateId equals template.Id
join channel in dbContext.Channels on template.ChannelId equals channel.Id
where slot.GroupId == groupId
select channel.Name
)
.Distinct()
.ToListAsync(cancellationToken);
var junctions = await (
from element in dbContext.JunctionElements
join junction in dbContext.JunctionTemplates
on element.JunctionTemplateId equals junction.Id
where element.GroupId == groupId
select junction.Name
)
.Distinct()
.ToListAsync(cancellationToken);
var fallback = await (
from template in dbContext.ScheduleTemplates
join channel in dbContext.Channels on template.ChannelId equals channel.Id
where template.FallbackGroupId == groupId
select channel.Name
)
.Distinct()
.ToListAsync(cancellationToken);
return UsageText.Join(
UsageText.Part("слоты каналов", slots),
UsageText.Part("врезки стыков", junctions),
UsageText.Part("аварийная группа каналов", fallback)
);
}
}
@@ -16,10 +16,19 @@ public static class GroupErrors
"Шоу или коллекция не найдены."
);
public static readonly Error InUse = Error.Conflict(
"Groups.InUse",
"Группа используется в сетке: на неё ссылается слот, врезка стыка или запасная группа шаблона."
);
/// <summary>
/// Где именно занята группа: без адреса «используется в сетке» отправляет обходить все каналы.
/// Пустой <paramref name="where"/> — связи неполны (слой без шаблона, шаблон без канала),
/// и тогда честнее общий текст, чем пустые скобки.
/// </summary>
public static Error InUse(string where) =>
Error.Conflict(
"Groups.InUse",
where.Length > 0
? $"Группа используется в сетке ({where}) — сначала отвяжите её там."
: "Группа используется в сетке: на неё ссылается слот, врезка стыка "
+ "или запасная группа шаблона."
);
public static readonly Error FilterNotSet = Error.Validation(
"Groups.FilterNotSet",
@@ -30,10 +30,44 @@ public sealed class DeleteJunctionCommandHandler(IAppDbContext dbContext)
t => t.DefaultJunctionId == junction.Id,
cancellationToken
);
// Факт и адрес — разные вопросы: имя канала берётся по цепочке связей, и её неполнота
// не должна превращать запрет в разрешение.
if (usedBySlot || usedByDefault)
return Result.Failure(TemplateErrors.JunctionInUse);
return Result.Failure(
TemplateErrors.JunctionInUse(await UsedByAsync(junction.Id, cancellationToken))
);
dbContext.JunctionTemplates.Remove(junction);
return Result.Success();
}
/// <summary>Кто держит стык: слоты каналов и каналы, где он выбран стыком по умолчанию.</summary>
private async Task<string> UsedByAsync(Guid junctionId, CancellationToken cancellationToken)
{
var slots = await (
from slot in dbContext.Slots
join layer in dbContext.GridLayers on slot.LayerId equals layer.Id
join template in dbContext.ScheduleTemplates on layer.TemplateId equals template.Id
join channel in dbContext.Channels on template.ChannelId equals channel.Id
where slot.JunctionBetweenId == junctionId || slot.JunctionAfterId == junctionId
select channel.Name
)
.Distinct()
.ToListAsync(cancellationToken);
var byDefault = await (
from template in dbContext.ScheduleTemplates
join channel in dbContext.Channels on template.ChannelId equals channel.Id
where template.DefaultJunctionId == junctionId
select channel.Name
)
.Distinct()
.ToListAsync(cancellationToken);
return UsageText.Join(
UsageText.Part("слоты каналов", slots),
UsageText.Part("стык по умолчанию у каналов", byDefault)
);
}
}
@@ -49,10 +49,14 @@ public static class TemplateErrors
"Врезка не найдена."
);
public static readonly Error JunctionInUse = Error.Conflict(
"Templates.JunctionInUse",
"Стык используется слотами — сначала отвяжите его."
);
/// <summary>Стык общий, поэтому держать его может слот чужого канала — называем какого.</summary>
public static Error JunctionInUse(string where) =>
Error.Conflict(
"Templates.JunctionInUse",
where.Length > 0
? $"Стык используется ({where}) — сначала отвяжите его там."
: "Стык используется слотами — сначала отвяжите его."
);
public static readonly Error JunctionGroupRequired = Error.Validation(
"Templates.JunctionGroupRequired",