fullstackhero/dotnet-starter-kit · error · NotFoundException
Image not found on product .
Error message
Image {command.ImageId} not found on product {command.ProductId}. What it means
After the product is found, Handle verifies that command.ImageId is one of product.Images; if not, it throws NotFoundException. This pre-emptively translates the domain's InvalidOperationException for an unknown image into a 404 so clients get NotFound, not a 500.
Solutions
- Fetch the product's images and confirm the ImageId belongs to this product before sending the command.
- Reload product images in the UI after any delete/add so the picker only offers current ids.
- If the image belongs to another product, use that product's id — images are not shared.
- Retry the flow after refreshing data if the image was just uploaded.
Example fix
// before
await mediator.Send(new SetProductThumbnailCommand(productId, imageId));
// after
var product = await dbContext.Products.Include(p => p.Images).FirstAsync(p => p.Id == productId, ct);
if (!product.Images.Any(i => i.Id == imageId))
throw new InvalidOperationException("Pick one of the product's own images");
await mediator.Send(new SetProductThumbnailCommand(productId, imageId), ct); Defensive patterns
Strategy: validation
Validate before calling
bool ok = (await dbContext.Products.Include(p => p.Images)
.FirstAsync(p => p.Id == productId, ct)).Images.Any(i => i.Id == imageId); Type guard
var image = product?.Images.FirstOrDefault(i => i.Id == imageId); if (image is null) return; // image not on this product
Try / catch
try { await mediator.Send(cmd, ct); }
catch (NotFoundException ex) when (ex.Message.StartsWith("Image")) { return Results.NotFound(ex.Message); } Prevention
- Populate image pickers from the product's current images immediately before submit.
- Never reuse image ids across products.
- Refresh image data after any upload/delete operation.
- Handle 404 for images distinctly so users get a 'refresh images' hint.
When it happens
Trigger: SetProductThumbnailCommand sent with an ImageId that is not present in the product's Images collection (image deleted first, image belongs to another product, or id typo).
Common situations: Frontend keeps a stale image list after another user removed the image; caller passes an image id from a different product; race between image deletion and thumbnail assignment.
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/c2c0be2908b3f1ab.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/SetProductThumbnail/SetProductThumbnailCommandHandler.cs:25
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)