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

  1. Query the Brands table (respecting tenant filters) to confirm the BrandId exists before calling the endpoint.
  2. If the brand was deleted, create the product with an existing brand or recreate the brand first.
  3. In multi-tenant setups, verify the request carries the correct tenant identifier so the brand row is visible.
  4. 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

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)