fullstackhero/dotnet-starter-kit · error · NotFoundException
Product not found.
Error message
Product {command.ProductId} not found. What it means
RestoreProductCommandHandler deliberately ignores the soft-delete filter (IgnoreQueryFilters([QueryFilters.SoftDelete])) so it can find soft-deleted rows, but still throws NotFoundException if no product row with command.ProductId exists at all (HTTP 404). Note: a product that was never deleted is found by this query too; a truly absent (hard-deleted or wrong-tenant/wrong-id) row 404s.
Solutions
- Confirm the id exists in the database (including soft-deleted rows) before restoring.
- Verify the tenant context matches the product's tenant — restore only bypasses the soft-delete filter, not tenant isolation.
- If the row was hard-deleted/purged, restore from backup or recreate the product; the API cannot recover it.
- Handle 404 in the trash-bin UI by refreshing the deleted-items list.
Example fix
// before
await api.restoreProduct(deletedId); // 404 if purged
// after
try {
await api.restoreProduct(deletedId);
notify('Product restored');
} catch (e) {
if (e.status === 404) {
notify('Product no longer recoverable');
refreshTrashBin();
return;
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// validate GUID format before restore attempt
const isGuid = (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
if (!isGuid(productId)) throw new Error('Invalid product id'); Try / catch
try {
await api.restoreProduct(productId);
} catch (e) {
if (e.status === 404) {
notify('Product is no longer recoverable');
refreshTrashBin();
return;
}
throw e;
} Prevention
- Refresh the trash-bin list before restoring items
- Remember tenant isolation still applies on restore — check tenant context
- Don't rely on restore for purged (hard-deleted) rows; check retention policy
When it happens
Trigger: POST restore for an id that was hard-deleted, never existed, is a mistyped GUID, or belongs to another tenant (tenant filter still applies since only the soft-delete filter is bypassed).
Common situations: Admin trash-bin UI restoring an item purged by retention policy; restoring across tenants after switching tenant context; ids copied from logs of another environment.
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/b9a6b5b88212105c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/RestoreProduct/RestoreProductCommandHandler.cs:21
using FSH.Modules.Catalog.Contracts.v1.Products;
using FSH.Modules.Catalog.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Catalog.Features.v1.Products.RestoreProduct;
public sealed class RestoreProductCommandHandler(CatalogDbContext dbContext)
: ICommandHandler<RestoreProductCommand, Guid>
{
public async ValueTask<Guid> Handle(RestoreProductCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var product = await dbContext.Products
.IgnoreQueryFilters([QueryFilters.SoftDelete])
.FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Product {command.ProductId} not found.");
product.Restore();
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return product.Id;
}
}
View on GitHub (pinned to 3f2959e683)