fullstackhero/dotnet-starter-kit · error · NotFoundException

Product not found.

Error message

Product {command.ProductId} not found.

What it means

DeleteProductCommandHandler loads the product (with IgnoreAutoIncludes so images are not tracked) and throws NotFoundException if no row matches command.ProductId. The API maps this to HTTP 404. The delete is a soft delete (interceptor converts Remove to an update), but it still requires an existing, tenant-visible row.

Solutions

  1. Verify the product id via GET /products/{id} before deleting; treat 404 on delete of an already-deleted product as success (idempotent handling).
  2. Refresh the product list in the UI before issuing delete on a stale row.
  3. If the product was soft-deleted and must be removed/restored, use the restore endpoint rather than delete.
  4. Confirm the request's tenant context matches the product's tenant.

Example fix

// before
await api.deleteProduct(id);
// after
try {
  await api.deleteProduct(id);
} catch (e) {
  if (e.status === 404) {
    // already gone — treat as idempotent success
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const exists = await api.getProduct(productId).then(() => true).catch(e => e.status !== 404);
if (!exists) return; // nothing to delete

Try / catch

try {
  await api.deleteProduct(productId);
} catch (e) {
  if (e.status === 404) return; // idempotent: already deleted
  throw e;
}

Prevention

When it happens

Trigger: DELETE /products/{id} with an unknown id, an already (soft-)deleted product hidden by the soft-delete query filter, or a product belonging to another tenant.

Common situations: UI keeps a stale list open while another operator deletes the product; client retries a delete that already succeeded (second call 404s); test ids from a different database.

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/fb4abbae56c8b340. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/DeleteProduct/DeleteProductCommandHandler.cs:22

using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Catalog.Features.v1.Products.DeleteProduct;

public sealed class DeleteProductCommandHandler(CatalogDbContext dbContext)
    : ICommandHandler<DeleteProductCommand, Unit>
{
    public async ValueTask<Unit> Handle(DeleteProductCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        // IgnoreAutoIncludes is load-bearing: if Product.Images (AutoInclude'd) load here, Remove() cascades Deleted onto them
        // and the soft-delete interceptor (rescues only owned refs) HARD-deletes them. Untracked keeps the delete a pure UPDATE so rows survive restore.
        var product = await dbContext.Products
            .IgnoreAutoIncludes()
            .FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Product {command.ProductId} not found.");

        dbContext.Products.Remove(product);
        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return Unit.Value;
    }
}

View on GitHub (pinned to 3f2959e683)