diff --git a/lib/commanded/commands/dispatcher.ex b/lib/commanded/commands/dispatcher.ex index c8133312..37f5371a 100644 --- a/lib/commanded/commands/dispatcher.ex +++ b/lib/commanded/commands/dispatcher.ex @@ -158,7 +158,6 @@ defmodule Commanded.Commands.Dispatcher do defp to_execution_context(%Pipeline{} = pipeline, %Payload{} = payload) do %Pipeline{ command: command, - command_uuid: command_uuid, causation_id: causation_id, metadata: metadata } = pipeline @@ -175,7 +174,8 @@ defmodule Commanded.Commands.Dispatcher do %ExecutionContext{ command: command, - causation_id: causation_id || command_uuid, + # `causation_id` is resolved at the router boundary (#624) + causation_id: causation_id, correlation_id: correlation_id, metadata: metadata, handler: handler_module, diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex index b8a7db0e..79bc5467 100644 --- a/lib/commanded/commands/router.ex +++ b/lib/commanded/commands/router.ex @@ -465,7 +465,12 @@ defmodule Commanded.Commands.Router do Options: - `causation_id` - an optional UUID used to identify the cause of the - command being dispatched. + command being dispatched. When the option is omitted, the command's + own `command_uuid` is used (preserving prior behaviour). Pass + `causation_id: nil` explicitly to mark the resulting events as chain + roots — they will be persisted with `causation_id` set to `NULL`, + letting `WHERE causation_id IS NULL` queries identify roots without + a self-join. - `command_uuid` - an optional UUID used to identify the command being dispatched. When omitted, one will be generated automatically. @@ -568,10 +573,17 @@ defmodule Commanded.Commands.Router do opts = Keyword.merge(@command_opts, opts) application = Keyword.fetch!(opts, :application) - causation_id = Keyword.get(opts, :causation_id) command_uuid = Router.fetch_command_uuid(opts) consistency = Keyword.fetch!(opts, :consistency) correlation_id = Keyword.get_lazy(opts, :correlation_id, &UUID.uuid7/0) + + # `Keyword.fetch/2` distinguishes absence from explicit `nil` (#624) + causation_id = + case Keyword.fetch(opts, :causation_id) do + {:ok, value} -> value + :error -> command_uuid + end + metadata = Keyword.fetch!(opts, :metadata) |> validate_metadata() retry_attempts = Keyword.get(opts, :retry_attempts) diff --git a/test/commands/correlation_causation_test.exs b/test/commands/correlation_causation_test.exs index 922c1bf2..a0473858 100644 --- a/test/commands/correlation_causation_test.exs +++ b/test/commands/correlation_causation_test.exs @@ -59,6 +59,16 @@ defmodule Commanded.Commands.CorrelationCasuationTest do assert event.causation_id == causation_id end + + test "should be `nil` on created event when `causation_id: nil` is passed explicitly" do + command = %OpenAccount{account_number: "ACC123", initial_balance: 500} + + :ok = BankRouter.dispatch(command, application: BankApp, causation_id: nil) + + [event] = EventStore.stream_forward(BankApp, "ACC123") |> Enum.to_list() + + assert is_nil(event.causation_id) + end end describe "`correlation_id`" do