fullstackhero/dotnet-starter-kit · error · NotFoundException
Product not found.
Error message
Product {command.ProductId} not found. What it means
UpdateProductCommandHandler.Handle loads the product by ProductId and throws NotFoundException when FirstOrDefaultAsync finds nothing, mapping the miss to a 404 at the API layer. Same contract as other Catalog handlers: unknown aggregate id in a command = NotFound.
Solutions
- Confirm the product id via the GET product endpoint before updating.
- Refresh the products list — the row may have been deleted concurrently.
- Verify the request targets the right tenant; tenant filters hide other tenants' rows.
- Fix client code that may be sending undefined/null mapped to a wrong Guid.
Example fix
// before
var product = await db.Products.FindAsync(productId); // may be null → later NRE
await mediator.Send(new UpdateProductCommand(productId, body));
// after
if (!await db.Products.AnyAsync(p => p.Id == productId, ct))
return Results.NotFound();
await mediator.Send(new UpdateProductCommand(productId, body), ct); Defensive patterns
Strategy: validation
Validate before calling
bool exists = await dbContext.Products.AnyAsync(p => p.Id == command.ProductId, ct); if (!exists) return Results.NotFound();
Type guard
var product = await dbContext.Products.FirstOrDefaultAsync(p => p.Id == id, ct); if (product is null) return Results.NotFound();
Try / catch
try { await mediator.Send(updateCommand, ct); }
catch (NotFoundException) { return Results.NotFound($"Product {id} not found"); } Prevention
- Re-fetch entity lists before edit forms submit; don't trust row ids from stale pages.
- Confirm ids are Guid-typed and non-empty before dispatching commands.
- In optimistic UI, refetch on 404 and remove the row locally.
- Check for concurrent deletes in collaborative tenants.
When it happens
Trigger: UpdateProductCommand dispatched with a ProductId that doesn't exist in dbContext.Products (deleted product, wrong tenant, malformed id from the client).
Common situations: Editing a product that was deleted in another browser tab; stale list page submitting updates for removed rows; multitenant deployment where the id belongs to a different tenant.
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/ddc4a40e4a280e97.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/UpdateProduct/UpdateProductCommandHandler.cs:20
using FSH.Framework.Core.Exceptions;
using FSH.Modules.Catalog.Contracts.v1.Products;
using FSH.Modules.Catalog.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Catalog.Features.v1.Products.UpdateProduct;
public sealed class UpdateProductCommandHandler(CatalogDbContext dbContext)
: ICommandHandler<UpdateProductCommand, Guid>
{
public async ValueTask<Guid> Handle(UpdateProductCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var product = await dbContext.Products
.FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Product {command.ProductId} not found.");
if (product.BrandId != command.BrandId)
{
bool brandExists = await dbContext.Brands
.AnyAsync(b => b.Id == command.BrandId, cancellationToken)
.ConfigureAwait(false);
if (!brandExists)
{
throw new NotFoundException($"Brand {command.BrandId} not found.");
}
}
if (product.CategoryId != command.CategoryId)
{
bool categoryExists = await dbContext.Categories
.AnyAsync(c => c.Id == command.CategoryId, cancellationToken)
.ConfigureAwait(false);
if (!categoryExists)View on GitHub (pinned to 3f2959e683)