{"record":{"id":"fb4abbae56c8b340","repo":"fullstackhero/dotnet-starter-kit","slug":"product-command-productid-not-found-fb4abb","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/DeleteProduct/DeleteProductCommandHandler.cs","lineNumber":22,"sourceCode":"using Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Catalog.Features.v1.Products.DeleteProduct;\n\npublic sealed class DeleteProductCommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<DeleteProductCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteProductCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        // IgnoreAutoIncludes is load-bearing: if Product.Images (AutoInclude'd) load here, Remove() cascades Deleted onto them\n        // and the soft-delete interceptor (rescues only owned refs) HARD-deletes them. Untracked keeps the delete a pure UPDATE so rows survive restore.\n        var product = await dbContext.Products\n            .IgnoreAutoIncludes()\n            .FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Product {command.ProductId} not found.\");\n\n        dbContext.Products.Remove(product);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":4,"sourceCodeEnd":29,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Products/DeleteProduct/DeleteProductCommandHandler.cs#L4-L29","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait api.deleteProduct(id);\n// after\ntry {\n  await api.deleteProduct(id);\n} catch (e) {\n  if (e.status === 404) {\n    // already gone — treat as idempotent success\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const exists = await api.getProduct(productId).then(() => true).catch(e => e.status !== 404);\nif (!exists) return; // nothing to delete","typeGuard":null,"tryCatchPattern":"try {\n  await api.deleteProduct(productId);\n} catch (e) {\n  if (e.status === 404) return; // idempotent: already deleted\n  throw e;\n}","preventionTips":["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"],"tags":["ef-core","not-found","soft-delete","delete","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"}