Repository: slashdotdash/commanded (elixir)

Issue:
Return execution result from command dispatch
`Router.dispatch/1` doesn't allow to retrieve the events produced by the aggregate in response to the command.

It could either be added as a breaking change to unify the API:

```elixir
# Would have previously returned :ok
iex> Router.dispatch(command)
{
  :ok, 
  %ExecutionResult{
    aggregate_uuid: aggregate_uuid, 
    aggregate_version: aggregate_version, 
    events: events,
    metadata: metadata,
  }
}
```

Or as a non-breaking change, using a new option

```elixir
iex> Router.dispatch(command)
:ok

iex> Router.dispatch(command, include_aggregate_version: true)
{:ok, aggregate_version}

iex> Router.dispatch(command, include_execution_result: true)
{
  :ok, 
  %ExecutionResult{
    aggregate_uuid: aggregate_uuid, 
    aggregate_version: aggregate_version, 
    events: events,
    metadata: metadata,
  }
}
```

Pull request:
Add include_execution_result option to dispatch
Fix #92

Patch:
diff --git a/lib/commanded/aggregates/aggregate.ex b/lib/commanded/aggregates/aggregate.ex
index 64f7d26..d270d99 100644
--- a/lib/commanded/aggregates/aggregate.ex
+++ b/lib/commanded/aggregates/aggregate.ex
@@ -71,10 +71,11 @@ defmodule Commanded.Aggregates.Aggregate do
 
   ## Return values
 
-  Returns `{:ok, aggregate_version, event_count}` on success, or `{:error, reason}` on failure.
+  Returns `{:ok, aggregate_version, event_count, events}` on success, or `{:error, reason}` on failure.
 
     - `aggregate_version` - the updated version of the aggregate after executing the command.
-    - `event_count` - number of events produced by the command, will be 0 when no events created,
+    - `event_count` - number of events produced by the command, will be 0 when no events created.
+    - `events` - events produced by the command, can be empty.
 
   """
   def execute(aggregate_module, aggregate_uuid, %ExecutionContext{} = context, timeout \\ 5_000) do
@@ -176,7 +177,7 @@ defmodule Commanded.Aggregates.Aggregate do
         {reply, state}
 
       none when none in [nil, []] ->
-        {{:ok, expected_version, 0}, state}
+        {{:ok, expected_version, 0, []}, state}
 
       events ->
         pending_events = List.wrap(events)
@@ -190,7 +191,7 @@ defmodule Commanded.Aggregates.Aggregate do
           aggregate_version: stream_version
         }
 
-        {{:ok, stream_version, length(pending_events)}, state}
+        {{:ok, stream_version, length(pending_events), pending_events}, state}
     end
   end
 
diff --git a/lib/commanded/commands/dispatcher.ex b/lib/commanded/commands/dispatcher.ex
index c596e4e..c871bc2 100644
--- a/lib/commanded/commands/dispatcher.ex
+++ b/lib/commanded/commands/dispatcher.ex
@@ -16,6 +16,7 @@ defmodule Commanded.Commands.Dispatcher do
       handler_function: nil,
       aggregate_module: nil,
       include_aggregate_version: nil,
+      include_execution_result: nil,
       identity: nil,
       identity_prefix: nil,
       timeout: nil,
@@ -25,6 +26,16 @@ defmodule Commanded.Commands.Dispatcher do
     ]
   end
 
+  defmodule ExecutionResult do
+    @moduledoc false
+    defstruct [
+      aggregate_uuid: nil,
+      aggregate_version: nil,
+      events: nil,
+      metadata: nil,
+    ]
+  end
+
   @doc """
   Dispatch the given command to the handler module for the aggregate root as identified
 
@@ -72,12 +83,12 @@ defmodule Commanded.Commands.Dispatcher do
       end
 
     case result do
-      {:ok, aggregate_version, event_count} ->
+      {:ok, aggregate_version, event_count, events} ->
         pipeline
         |> Pipeline.assign(:aggregate_version, aggregate_version)
         |> Pipeline.assign(:event_count, event_count)
         |> after_dispatch(payload)
-        |> respond_with_success(payload, aggregate_version)
+        |> respond_with_success(payload, events)
 
       {:error, error} ->
         pipeline
@@ -105,11 +116,21 @@ defmodule Commanded.Commands.Dispatcher do
     }
   end
 
-  defp respond_with_success(%Pipeline{} = pipeline, %Payload{include_aggregate_version: include_aggregate_version}, aggregate_version) do
+  defp respond_with_success(%Pipeline{} = pipeline, payload, events) do
     response =
-      case include_aggregate_version do
-        true -> {:ok, aggregate_version}
-        false -> :ok
+      case payload do
+        %{include_execution_result: true} ->
+          {
+            :ok,
+            %ExecutionResult{
+              aggregate_uuid: pipeline.assigns.aggregate_uuid,
+              aggregate_version: pipeline.assigns.aggregate_version,
+              events: events,
+              metadata: nil,
+            }
+          }
+        %{include_aggregate_version: true} -> {:ok, pipeline.assigns.aggregate_version}
+        _ -> :ok
       end
 
     Pipeline.respond(pipeline, response)
diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex
index ffa798f..ba9a823 100644
--- a/lib/commanded/commands/router.ex
+++ b/lib/commanded/commands/router.ex
@@ -89,6 +89,16 @@ defmodule Commanded.Commands.Router do
   This is useful when you need to wait for an event handler (e.g. a read model
   projection) to be up-to-date before continuing or querying its data.
 
+  ## Execution results
+
+  You can also choose to include the execution result as part of the dispatch result by
+  setting `include_execution_result` true.
+
+      {:ok, execution_result} = BankRouter.dispatch(command, include_execution_result: true)
+
+  You can use this if you need to get information from the events produced by the aggregate
+  but you can't afford to wait for the events to be projected.
+
   ## Metadata
 
   You can associate metadata with all events created by the command.
@@ -116,6 +126,7 @@ defmodule Commanded.Commands.Router do
       @default_lifespan Commanded.Aggregates.DefaultLifespan
       @default_metadata %{}
       @include_aggregate_version false
+      @include_execution_result false
     end
   end
 
@@ -247,13 +258,19 @@ defmodule Commanded.Commands.Router do
 
           - `:include_aggregate_version` set to true to include the aggregate
             stream version in the success response: `{:ok, aggregate_version}`
-            The default is false, to return just `:ok`
+            The default is false, to return just `:ok`.
+
+          - `:include_execution_result` set to true to include more information
+            about the dispatch, like the aggregate name, uuid, and the produced
+            events. Overrides `include_aggregate_version`. The default is false,
+            to return just `:ok`. See `Commanded.Commands.Dispatcher.ExecutionResult`.
 
           - `:metadata` - An optional map containing key/value pairs comprising
             the metadata to be associated with all events created by the command.
 
-      Returns `:ok` on success, unless `:include_aggregate_version` is enabled
-      where it returns `{:ok, aggregate_version}`.
+      Returns `:ok` on success, unless `:include_aggregate_version` or
+      `:include_execution_result` is enabled, where it respectively returns
+      `{:ok, aggregate_version}` or `{:ok, execution_result}`.
       """
       @spec dispatch(command :: struct, timeout_or_opts :: integer | :infinity | keyword()) :: :ok | {:error, :consistency_timeout} | {:error, reason :: term}
       def dispatch(command, timeout_or_opts)
@@ -266,6 +283,7 @@ defmodule Commanded.Commands.Router do
         metadata = Keyword.get(opts, :metadata) || @default_metadata
         timeout = Keyword.get(opts, :timeout) || unquote(timeout) || @default_dispatch_timeout
         include_aggregate_version = Keyword.get(opts, :include_aggregate_version) || @include_aggregate_version
+        include_execution_result = Keyword.get(opts, :include_execution_result) || @include_execution_result
         lifespan = Keyword.get(opts, :lifespan) || unquote(lifespan) || @default_lifespan
 
         default_identity = unquote(identity)
@@ -292,6 +310,7 @@ defmodule Commanded.Commands.Router do
           identity: identity,
           identity_prefix: identity_prefix,
           include_aggregate_version: include_aggregate_version,
+          include_execution_result: include_execution_result,
           timeout: timeout,
           lifespan: lifespan,
           metadata: metadata,


Test patch:
diff --git a/test/aggregates/event_persistence_test.exs b/test/aggregates/event_persistence_test.exs
index 5d1a723..a9beaba 100644
--- a/test/aggregates/event_persistence_test.exs
+++ b/test/aggregates/event_persistence_test.exs
@@ -55,7 +55,7 @@ defmodule Commanded.Entities.EventPersistenceTest do
 
     {:ok, ^aggregate_uuid} = Commanded.Aggregates.Supervisor.open_aggregate(ExampleAggregate, aggregate_uuid)
 
-    {:ok, 10, 10} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 10}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 10, 10, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 10}, handler: AppendItemsHandler, function: :handle})
 
     recorded_events = EventStore.stream_forward(aggregate_uuid, 0) |> Enum.to_list()
 
@@ -72,8 +72,8 @@ defmodule Commanded.Entities.EventPersistenceTest do
 
     {:ok, ^aggregate_uuid} = Commanded.Aggregates.Supervisor.open_aggregate(ExampleAggregate, aggregate_uuid)
 
-    {:ok, 1, 1} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 1}, handler: AppendItemsHandler, function: :handle})
-    {:ok, 1, 0} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %NoOp{}, handler: ExampleAggregate, function: :noop})
+    {:ok, 1, 1, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 1}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 1, 0, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %NoOp{}, handler: ExampleAggregate, function: :noop})
 
     recorded_events = EventStore.stream_forward(aggregate_uuid, 0) |> Enum.to_list()
     assert length(recorded_events) == 1
@@ -87,7 +87,7 @@ defmodule Commanded.Entities.EventPersistenceTest do
     metadata = %{"ip_address" => "127.0.0.1"}
     context = %ExecutionContext{command: %AppendItems{count: 10}, metadata: metadata, handler: AppendItemsHandler, function: :handle}
 
-    {:ok, 10, 10} = Aggregate.execute(ExampleAggregate, aggregate_uuid, context)
+    {:ok, 10, 10, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, context)
 
     recorded_events = EventStore.stream_forward(aggregate_uuid, 0) |> Enum.to_list()
 
@@ -101,7 +101,7 @@ defmodule Commanded.Entities.EventPersistenceTest do
 
     {:ok, ^aggregate_uuid} = Commanded.Aggregates.Supervisor.open_aggregate(ExampleAggregate, aggregate_uuid)
 
-    {:ok, 10, 10} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 10}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 10, 10, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 10}, handler: AppendItemsHandler, function: :handle})
 
     Commanded.Helpers.Process.shutdown(ExampleAggregate, aggregate_uuid)
 
@@ -119,9 +119,9 @@ defmodule Commanded.Entities.EventPersistenceTest do
 
     {:ok, ^aggregate_uuid} = Commanded.Aggregates.Supervisor.open_aggregate(ExampleAggregate, aggregate_uuid)
 
-    {:ok, 100, 100} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 100}, handler: AppendItemsHandler, function: :handle})
-    {:ok, 200, 100} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 100}, handler: AppendItemsHandler, function: :handle})
-    {:ok, 201, 1} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 1}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 100, 100, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 100}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 200, 100, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 100}, handler: AppendItemsHandler, function: :handle})
+    {:ok, 201, 1, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, %ExecutionContext{command: %AppendItems{count: 1}, handler: AppendItemsHandler, function: :handle})
 
     Commanded.Helpers.Process.shutdown(ExampleAggregate, aggregate_uuid)
 
@@ -142,7 +142,7 @@ defmodule Commanded.Entities.EventPersistenceTest do
 
     context = %ExecutionContext{command: %AppendItems{count: 1}, handler: AppendItemsHandler, function: :handle}
 
-    {:ok, 1, 1} = Aggregate.execute(ExampleAggregate, aggregate_uuid, context)
+    {:ok, 1, 1, _events} = Aggregate.execute(ExampleAggregate, aggregate_uuid, context)
 
     recorded_events = EventStore.stream_forward(prefix <> aggregate_uuid, 0) |> Enum.to_list()
     assert length(recorded_events) == 1
diff --git a/test/aggregates/execute_command_for_aggregate_test.exs b/test/aggregates/execute_command_for_aggregate_test.exs
index 2b1a1eb..2085bb4 100644
--- a/test/aggregates/execute_command_for_aggregate_test.exs
+++ b/test/aggregates/execute_command_for_aggregate_test.exs
@@ -9,7 +9,7 @@ defmodule Commanded.Entities.ExecuteCommandForAggregateTest do
   alias Commanded.Helpers
 
   @registry_provider Application.get_env(:commanded, :registry_provider, Registry)
-  
+
   test "execute command against an aggregate" do
     account_number = UUID.uuid4
 
@@ -18,13 +18,14 @@ defmodule Commanded.Entities.ExecuteCommandForAggregateTest do
     command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
     context = %ExecutionContext{command: command, handler: BankAccount, function: :open_account}
 
-    {:ok, 1, 1} = Aggregate.execute(BankAccount, account_number, context)
+    {:ok, 1, 1, events} = Aggregate.execute(BankAccount, account_number, context)
 
     Helpers.Process.shutdown(BankAccount, account_number)
 
     # reload aggregate to fetch persisted events from event store and rebuild state by applying saved events
     {:ok, ^account_number} = Commanded.Aggregates.Supervisor.open_aggregate(BankAccount, account_number)
 
+    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1000}]
     assert Aggregate.aggregate_version(BankAccount, account_number) == 1
     assert Aggregate.aggregate_state(BankAccount, account_number) == %BankAccount{account_number: account_number, balance: 1_000, state: :active}
   end
@@ -37,13 +38,14 @@ defmodule Commanded.Entities.ExecuteCommandForAggregateTest do
     command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
     context = %ExecutionContext{command: command, handler: OpenAccountHandler, function: :handle}
 
-    {:ok, 1, 1} = Aggregate.execute(BankAccount, account_number, context)
+    {:ok, 1, 1, events} = Aggregate.execute(BankAccount, account_number, context)
 
     Helpers.Process.shutdown(BankAccount, account_number)
 
     # reload aggregate to fetch persisted events from event store and rebuild state by applying saved events
     {:ok, ^account_number} = Commanded.Aggregates.Supervisor.open_aggregate(BankAccount, account_number)
 
+    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1000}]
     assert Aggregate.aggregate_version(BankAccount, account_number) == 1
     assert Aggregate.aggregate_state(BankAccount, account_number) == %BankAccount{account_number: account_number, balance: 1_000, state: :active}
   end
@@ -56,7 +58,7 @@ defmodule Commanded.Entities.ExecuteCommandForAggregateTest do
     command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
     context = %ExecutionContext{command: command, handler: OpenAccountHandler, function: :handle}
 
-    {:ok, 1, 1} = Aggregate.execute(BankAccount, account_number, context)
+    {:ok, 1, 1, _events} = Aggregate.execute(BankAccount, account_number, context)
 
     state_before = Aggregate.aggregate_state(BankAccount, account_number)
 
diff --git a/test/commands/routing_commands_test.exs b/test/commands/routing_commands_test.exs
index dac00df..b635f78 100644
--- a/test/commands/routing_commands_test.exs
+++ b/test/commands/routing_commands_test.exs
@@ -5,6 +5,8 @@ defmodule Commanded.Commands.RoutingCommandsTest do
   alias Commanded.ExampleDomain.BankAccount
   alias Commanded.ExampleDomain.{OpenAccountHandler,DepositMoneyHandler,WithdrawMoneyHandler}
   alias Commanded.ExampleDomain.BankAccount.Commands.{OpenAccount,CloseAccount,DepositMoney,WithdrawMoney}
+  alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
+  alias Commanded.Commands.Dispatcher.ExecutionResult
 
   defmodule UnregisteredCommand, do: defstruct [aggregate_uuid: UUID.uuid4]
 
@@ -263,4 +265,19 @@ defmodule Commanded.Commands.RoutingCommandsTest do
     assert :ok == MultiCommandHandlerRouter.dispatch(%OpenAccount{account_number: "ACC123", initial_balance: 1_000}, metadata: metadata)
     assert :ok == MultiCommandHandlerRouter.dispatch(%DepositMoney{account_number: "ACC123", amount: 100}, metadata: metadata)
   end
+
+  describe "include execution result" do
+    test "should return created events" do
+      assert MultiCommandHandlerRouter.dispatch(%OpenAccount{account_number: "ACC123", initial_balance: 1_000}, include_execution_result: true) ==
+        {
+          :ok,
+          %ExecutionResult{
+            aggregate_uuid: "ACC123",
+            aggregate_version: 1,
+            events: [%BankAccountOpened{account_number: "ACC123", initial_balance: 1000}],
+            metadata: nil
+          }
+        }
+    end
+  end
 end