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

  1. Fetch the product's images and confirm the ImageId belongs to this product before sending the command.
  2. Reload product images in the UI after any delete/add so the picker only offers current ids.
  3. If the image belongs to another product, use that product's id — images are not shared.
  4. 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

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)