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
- Verify the product id via GET /products/{id} before deleting; treat 404 on delete of an already-deleted product as success (idempotent handling).
- Refresh the product list in the UI before issuing delete on a stale row.
- If the product was soft-deleted and must be removed/restored, use the restore endpoint rather than delete.
- 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
- Refresh lists after any delete and before issuing deletes from stale views
- Treat delete 404 as success (idempotent delete pattern)
- Confirm tenant context before delete operations
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)