Remove NotFoundException; services return null on lookup miss

A missing record isn't exceptional — services now return null (logged
at Info) instead of throwing, and endpoints map null to 404. Agent and
import toolsets route not-found through their existing OrNotFound
result pattern rather than a caught exception.
This commit is contained in:
James Wampler
2026-08-06 21:22:40 -07:00
parent 189ebf3237
commit 2ccebb31eb
22 changed files with 305 additions and 212 deletions
+11 -9
View File
@@ -74,7 +74,8 @@ public class BeatService(
];
}
public async Task<Beat> CreateAsync(Guid chapterId, CreateBeatRequest request, CancellationToken ct = default)
/// <summary>Null when no chapter has this id — a lookup miss is expected, not exceptional.</summary>
public async Task<Beat?> CreateAsync(Guid chapterId, CreateBeatRequest request, CancellationToken ct = default)
{
Guard.Default(chapterId, nameof(chapterId));
Guard.Null(request, nameof(request));
@@ -85,8 +86,8 @@ public class BeatService(
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == chapterId, ct);
if (chapter is null)
{
logger.LogWarning("Rejected beat creation: chapter {ChapterId} not found", chapterId);
throw new NotFoundException(nameof(Chapter), chapterId);
logger.LogInformation("Rejected beat creation: chapter {ChapterId} not found", chapterId);
return null;
}
await ValidateReferencesAsync(chapter, request.CharacterId, request.SceneId, ct);
@@ -131,10 +132,10 @@ public class BeatService(
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == beat.ChapterId, ct);
if (chapter is null)
{
// The beat's own chapter should always exist via the FK — this is an
// invariant failing, not a caller mistake, so it stays exceptional.
// The beat's own chapter should always exist via the FK — an invariant
// failing, not a caller mistake, but still not found so still just null.
logger.LogError("Beat {BeatId} references chapter {ChapterId} which does not exist", id, beat.ChapterId);
throw new NotFoundException(nameof(Chapter), beat.ChapterId);
return null;
}
await ValidateReferencesAsync(chapter, request.CharacterId, request.SceneId, ct);
@@ -178,7 +179,8 @@ public class BeatService(
/// Renumbers a chapter's beats to match the order given. Sending the whole list beats
/// patching sort orders one at a time, which is fiddly to get right from a drag handle.
/// </summary>
public async Task<IReadOnlyList<Beat>> ReorderAsync(
/// <summary>Null when the chapter carries a beat id it does not own — a lookup miss is expected, not exceptional.</summary>
public async Task<IReadOnlyList<Beat>?> ReorderAsync(
Guid chapterId, ReorderBeatsRequest request, CancellationToken ct = default)
{
Guard.Default(chapterId, nameof(chapterId));
@@ -192,8 +194,8 @@ public class BeatService(
var missing = request.BeatIds.Where(id => beats.All(b => b.Id != id)).ToList();
if (missing.Count > 0)
{
logger.LogWarning("Reorder for chapter {ChapterId} referenced missing beat {BeatId}", chapterId, missing[0]);
throw new NotFoundException(nameof(Beat), missing[0]);
logger.LogInformation("Reorder for chapter {ChapterId} referenced missing beat {BeatId}", chapterId, missing[0]);
return null;
}
// Listed beats take the order given; anything omitted keeps its relative position