{"record":{"id":"b9a6b5b88212105c","repo":"fullstackhero/dotnet-starter-kit","slug":"product-command-productid-not-found-b9a6b5","errorCode":null,"errorMessage":"Product {command.ProductId} not found.","messagePattern":"Product (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Products/RestoreProduct/RestoreProductCommandHandler.cs","lineNumber":21,"sourceCode":"using FSH.Modules.Catalog.Contracts.v1.Products;\nusing FSH.Modules.Catalog.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Catalog.Features.v1.Products.RestoreProduct;\n\npublic sealed class RestoreProductCommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<RestoreProductCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(RestoreProductCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var product = await dbContext.Products\n            .IgnoreQueryFilters([QueryFilters.SoftDelete])\n            .FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Product {command.ProductId} not found.\");\n\n        product.Restore();\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return product.Id;\n    }\n}\n","sourceCodeStart":3,"sourceCodeEnd":28,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Products/RestoreProduct/RestoreProductCommandHandler.cs#L3-L28","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait api.restoreProduct(deletedId); // 404 if purged\n// after\ntry {\n  await api.restoreProduct(deletedId);\n  notify('Product restored');\n} catch (e) {\n  if (e.status === 404) {\n    notify('Product no longer recoverable');\n    refreshTrashBin();\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// validate GUID format before restore attempt\nconst 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);\nif (!isGuid(productId)) throw new Error('Invalid product id');","typeGuard":null,"tryCatchPattern":"try {\n  await api.restoreProduct(productId);\n} catch (e) {\n  if (e.status === 404) {\n    notify('Product is no longer recoverable');\n    refreshTrashBin();\n    return;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["ef-core","not-found","soft-delete","restore","http-404"],"backgroundTag":"entity-not-found","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}