{"record":{"id":"934391d95c12e2e4","repo":"fullstackhero/dotnet-starter-kit","slug":"channel-not-found-listchannelmessagesqueryhandler","errorCode":null,"errorMessage":"Channel not found.","messagePattern":"Channel not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Messages/ListChannelMessages/ListChannelMessagesQueryHandler.cs","lineNumber":30,"sourceCode":"\npublic sealed class ListChannelMessagesQueryHandler(\n    ChatDbContext db,\n    ICurrentUser currentUser,\n    IMediator mediator)\n    : IQueryHandler<ListChannelMessagesQuery, ReadOnlyCollection<MessageDto>>\n{\n    public async ValueTask<ReadOnlyCollection<MessageDto>> Handle(\n        ListChannelMessagesQuery query,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Channel not found.\");\n        channel.RequireMember(currentUserId);\n\n        // Top-level only (no thread replies). Guid v7 monotonic → Id desc = time desc.\n        IQueryable<Domain.Message> q = db.Messages\n            .Where(m => m.ChannelId == query.ChannelId && m.ParentMessageId == null);\n\n        if (query.Before is { } beforeId)\n        {\n            q = q.Where(m => m.Id.CompareTo(beforeId) < 0);\n        }\n\n        var rows = await q\n            .OrderByDescending(m => m.Id)\n            .Take(query.PageSize)\n            .Include(m => m.Attachments)\n            .AsNoTracking()\n            .ToListAsync(cancellationToken)\n            .ConfigureAwait(false);","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Messages/ListChannelMessages/ListChannelMessagesQueryHandler.cs#L12-L48","documentation":"The handler looks up db.Channels by query.ChannelId and throws NotFoundException('Channel not found.') (HTTP 404) when no row matches. Thrown before the membership check so unauthorized callers cannot probe channel membership.","triggerScenarios":"Listing messages for a ChannelId that does not exist, was deleted, or belongs to another tenant (filtered out by the tenant query filter).","commonSituations":"Client kept a channel id after the channel was deleted, cross-tenant id reuse, or a typo'd/wrong route parameter.","solutions":["Confirm the channel id exists in the current tenant before listing messages","Refresh the channel list in the client to drop stale ids","Verify tenant resolution (header/subdomain) matches the channel's tenant","Check the DB for the channel row / soft-delete state"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const channel = channels.find(c => c.id === channelId);\nif (!channel) throw new Error(`unknown channel ${channelId}`);","typeGuard":"function hasChannel(c: Channel | undefined): c is Channel { return c !== undefined; }","tryCatchPattern":"try { return await listChannelMessages(channelId); }\ncatch (e) { if (isNotFound(e)) { dropChannelFromSidebar(channelId); return []; } throw e; }","preventionTips":["Revalidate channel ids after deletes","Match tenant context with channel tenant","Clean stale ids from routing state","Return empty + message on 404 rather than crashing"],"tags":["chat","not-found","channel"],"backgroundTag":"entity-not-found","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}