fullstackhero/dotnet-starter-kit · error · NotFoundException
Brand not found.
Error message
Brand {command.BrandId} not found. What it means
CreateProductCommandHandler verifies the referenced Brand exists before creating a Product. If no Brand row matches command.BrandId, it throws NotFoundException, which the API layer maps to HTTP 404. This guards the FK relationship at the application layer before an insert is attempted.
Solutions
- Query the Brands table (respecting tenant filters) to confirm the BrandId exists before calling the endpoint.
- If the brand was deleted, create the product with an existing brand or recreate the brand first.
- In multi-tenant setups, verify the request carries the correct tenant identifier so the brand row is visible.
- If the brand should exist, check whether you are pointing at the right connection string/database/environment.
Example fix
// before
await api.createProduct({ sku: 'X', name: 'X', brandId: cachedBrandId });
// after
const brand = await api.getBrand(cachedBrandId); // throws/404s early if stale
if (!brand) {
const fresh = await api.listBrands();
cachedBrandId = fresh.items[0].id;
}
await api.createProduct({ sku: 'X', name: 'X', brandId: cachedBrandId }); Defensive patterns
Strategy: validation
Validate before calling
// client-side pre-check
const brand = await api.getBrand(brandId).catch(() => null);
if (!brand) throw new Error(`Brand ${brandId} does not exist`); Try / catch
try {
await api.createProduct(payload);
} catch (e) {
if (e.status === 404 && /Brand .* not found/.test(e.message ?? '')) {
showFieldError('brandId', 'Selected brand no longer exists');
return;
}
throw e;
} Prevention
- Populate brand pickers from a live API query, never from cached constants
- Revalidate cached brand ids on form load
- In multi-tenant apps, scope brand lists to the current tenant
When it happens
Trigger: POST /products (v1) with a BrandId that does not exist in the Brands table — e.g. a deleted brand, a brand from another tenant filtered out by the tenant query filter, or a fabricated/mistyped GUID.
Common situations: Client caches brand IDs that were later deleted; multi-tenant apps where the brand belongs to a different tenant so the global query filter hides it; test fixtures seeding brands into a different database than the API uses.
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/aaeb719f495aee0e.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/CreateProduct/CreateProductCommandHandler.cs:24
using FSH.Modules.Catalog.Domain;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Catalog.Features.v1.Products.CreateProduct;
public sealed class CreateProductCommandHandler(CatalogDbContext dbContext)
: ICommandHandler<CreateProductCommand, Guid>
{
public async ValueTask<Guid> Handle(CreateProductCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
bool brandExists = await dbContext.Brands
.AnyAsync(b => b.Id == command.BrandId, cancellationToken)
.ConfigureAwait(false);
if (!brandExists)
{
throw new NotFoundException($"Brand {command.BrandId} not found.");
}
bool categoryExists = await dbContext.Categories
.AnyAsync(c => c.Id == command.CategoryId, cancellationToken)
.ConfigureAwait(false);
if (!categoryExists)
{
throw new NotFoundException($"Category {command.CategoryId} not found.");
}
var product = Product.Create(
command.Sku,
command.Name,
command.Description,
command.BrandId,
command.CategoryId,
new Money(command.PriceAmount, command.PriceCurrency),
command.Stock);View on GitHub (pinned to 3f2959e683)