fullstackhero/dotnet-starter-kit · error · NotFoundException
Product not found.
Error message
Product {command.ProductId} not found. What it means
SetProductThumbnailCommandHandler.Handle looks up the product by ProductId and throws FSH's NotFoundException when no row matches, so the API returns a framework-mapped 404 instead of a null-reference/500. It is the module's contract for 'the product this command targets does not exist'.
Solutions
- Verify the ProductId exists (query the products list/GET endpoint) and resend with a valid id.
- Check the request is going to the correct tenant — tenant isolation filters Products per tenant.
- If the id comes from a previous screen, refresh the data instead of reusing a stale id.
- If the product should exist, confirm it was not deleted by a concurrent user or cleanup job.
Example fix
// before await mediator.Send(new SetProductThumbnailCommand(productId, imageId)); // after var exists = await dbContext.Products.AnyAsync(p => p.Id == productId, ct); if (!exists) return; // or surface 404 to the user before sending await mediator.Send(new SetProductThumbnailCommand(productId, imageId), ct);
Defensive patterns
Strategy: validation
Validate before calling
// C# caller
bool exists = await dbContext.Products.AnyAsync(p => p.Id == command.ProductId, ct);
if (!exists) throw new NotFoundException($"Product {command.ProductId} not found."); Type guard
var product = await dbContext.Products.FirstOrDefaultAsync(p => p.Id == id, ct); if (product is null) return null; // narrow before use
Try / catch
try { await mediator.Send(cmd, ct); }
catch (NotFoundException ex) { logger.LogWarning(ex, "Product missing"); return Results.NotFound(); } Prevention
- Always resolve product ids from a fresh API list, never from long-lived client caches.
- Include tenant context in requests so the right rows are visible.
- Treat NotFoundException as an expected 404, not a bug, in clients.
- Log the attempted ProductId to spot stale-id patterns.
When it happens
Trigger: Mediator command SetProductThumbnailCommand dispatched with a ProductId that has no row in dbContext.Products (hard delete, wrong tenant, or fabricated/typo id). FirstOrDefaultAsync returns null and the ?? throw fires.
Common situations: Client cached a product id after the product was deleted; id passed from another tenant under tenant isolation; test fixtures using random Guids; caller swapped ImageId/ProductId fields in the payload.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/e2415a4417ade015.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/SetProductThumbnail/SetProductThumbnailCommandHandler.cs:19
using FSH.Framework.Core.Exceptions;
using FSH.Modules.Catalog.Contracts.v1.Products.SetProductThumbnail;
using FSH.Modules.Catalog.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Catalog.Features.v1.Products.SetProductThumbnail;
public sealed class SetProductThumbnailCommandHandler(CatalogDbContext dbContext)
: ICommandHandler<SetProductThumbnailCommand, Unit>
{
public async ValueTask<Unit> Handle(SetProductThumbnailCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var product = await dbContext.Products
.FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Product {command.ProductId} not found.");
// Domain throws InvalidOperationException for unknown imageId; translate to a
// framework-aware 404 so the API surfaces NotFound rather than a 500.
if (!product.Images.Any(i => i.Id == command.ImageId))
{
throw new NotFoundException($"Image {command.ImageId} not found on product {command.ProductId}.");
}
product.SetThumbnail(command.ImageId);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
View on GitHub (pinned to 3f2959e683)