kgrzybek/modular-monolith-with-ddd · error · InvalidCommandException

Pricelist item for deactivation must exist.

Error message

Pricelist item for deactivation must exist.

What it means

Thrown by DeactivatePriceListItemCommandHandler when _aggregateStore.Load returns null for the PriceListItemId. Deactivation requires the aggregate to exist; a missing item aborts with InvalidCommandException (HTTP 400).

Source

Thrown at src/Modules/Payments/Application/PriceListItems/DeactivatePriceListItem/DeactivatePriceListItemCommandHandler.cs:23

namespace CompanyName.MyMeetings.Modules.Payments.Application.PriceListItems.DeactivatePriceListItem
{
    internal class DeactivatePriceListItemCommandHandler : ICommandHandler<DeactivatePriceListItemCommand>
    {
        private readonly IAggregateStore _aggregateStore;

        public DeactivatePriceListItemCommandHandler(IAggregateStore aggregateStore)
        {
            _aggregateStore = aggregateStore;
        }

        public async Task Handle(DeactivatePriceListItemCommand command, CancellationToken cancellationToken)
        {
            var priceListItem = await _aggregateStore.Load(new PriceListItemId(command.PriceListItemId));

            if (priceListItem == null)
            {
                throw new InvalidCommandException(["Pricelist item for deactivation must exist."]);
            }

            priceListItem.Deactivate();

            _aggregateStore.AppendChanges(priceListItem);
        }
    }
}

View on GitHub (pinned to 91c8ef24b4)

Solutions

  1. Verify the item exists before deactivation; treat 'already gone' as success if idempotent.
  2. Pre-check existence in the controller and return 404/204.
  3. Refresh the admin list to drop stale ids.
  4. Map InvalidCommandException to 400/404 at the API boundary.

Example fix

// before
await _commandDispatcher.SendAsync(new DeactivatePriceListItemCommand(itemId));

// after
var item = await _priceListQueries.GetItemAsync(itemId);
if (item is null) return NoContent(); // already deactivated/absent
await _commandDispatcher.SendAsync(new DeactivatePriceListItemCommand(itemId));
Defensive patterns

Strategy: validation

Validate before calling

var item = await _priceListQueries.GetItemAsync(itemId);
if (item is null) return NoContent(); // already deactivated
await _commandDispatcher.SendAsync(new DeactivatePriceListItemCommand(itemId));

Type guard

public static bool IsValidItemId(DeactivatePriceListItemCommand c) =>
    c.PriceListItemId != Guid.Empty;

Try / catch

try { await _commandDispatcher.SendAsync(cmd); }
catch (InvalidCommandException ex) when (ex.Errors.Any(m => m.Contains("must exist")))
{ return NoContent(); } // idempotent deactivate

Prevention

When it happens

Trigger: Dispatching DeactivatePriceListItemCommand for an id with no aggregate: already deleted, wrong id, wrong tenant, or missing event stream.

Common situations: Bulk deactivate job references stale ids; admin deactivates from a cached list; idempotent deactivate of an already-removed item.

Related errors


AI-assisted analysis of kgrzybek/modular-monolith-with-ddd@91c8ef24b4 (2026-08-13). Data as JSON: /api/errors/0fd092a2fd6ee50d. Report an issue: GitHub.