Repository: commanded/commanded (elixir)

Issue:
Misleading error message when file has a syntax error
I have run into this twice where the overloaded `Code.ensure_compiled?` error is overloaded in Commanded and displays a canned message about missing aliases.

For example, I was missing an alias in this case for `AuditDenied` and `DenyAudit`, but the error makes you think something is wrong in the router, not in the aggregate.

```elixir
defmodule Capacitor.Aggregates.Audit do
  alias Capacitor.Aggregates.Audit
  alias Capacitor.Commands.{ApproveAudit, CreateAudit}
  alias Capacitor.Events.{AuditApproved, AuditCreated}

 # ...
  def execute(%Audit{status: :open}, %DenyAudit{} = command) do
    AuditDenied.new(command)
  end
end
```

Here is the error I received trying to run `mix compile`

<img width="829" alt="Screen Shot 2020-03-27 at 9 37 23 AM" src="https://user-images.githubusercontent.com/16150/77773343-178a9580-700f-11ea-9d29-d11477d95100.png">

This sends you to the `router.ex` to look for a missing alias only to find it's another file.

---

I had a similar issue yesterday but that time it was a typo in one of my Commands. The only way I was able to find it was to comment out chunks of code and find what made `mix compile` pass.

Is there any way to make this error more friendly to point to where the issue in fact is?

Pull request description:
Remove router module compile-time checking
Compile-time checking for router dependencies causes compilation errors to be hidden, making it difficult to locate the error to be fixed. As an example a compiler error with an aggregate module simply returns the error message: "module `Aggregate` does not exist".  

This pull request removes the compile-time checking and reworks the `Commanded.Commands.Router.dispatch` macro to work without attempting to load the dependent modules. 

Fixed #358.

Patch:
diff --git a/guides/Application.md b/guides/Application.md
index faa55a3..ba28a9b 100644
--- a/guides/Application.md
+++ b/guides/Application.md
@@ -1,6 +1,6 @@
 # Application
 
-Commanded allows you to define, supervise, and start your own application module. To use Commanded you must create at least one application module. You can create multiple Commanded applications which will run independently, each using its own separately configured event store.
+Commanded allows you to define, supervise, and start your own application module. To use Commanded you must create at least one application. You can create multiple Commanded applications which will run independently, each using its own separately configured event store.
 
 The application expects at least an `:otp_app` option to be specified. It should point to an OTP application containing the application's configuration.
 
@@ -70,7 +70,7 @@ defmodule MyApp.Application do
 end
 ```
 
-Once you have defined a router, you can dispatch a command using the application module:
+Once you have defined a router you can dispatch a command using the application module:
 
 ```elixir
 :ok = MyApp.Application.dispatch(%RegisterCustomer{id: UUID.uuid4(), name: "Ben"})
diff --git a/lib/application.ex b/lib/application.ex
index 3c12daf..fa357d4 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -72,6 +72,15 @@ defmodule Commanded.Application do
 
   See `Commanded.Commands.CompositeRouter` for details.
 
+  ## Command dispatch
+
+  Once a router has been configured you can dispatch a command via the
+  application:
+
+      :ok = MyApp.dispatch(command, opts)
+
+  See `c:dispatch/1` and `c:dispatch/2` for details.
+
   ## Dynamic named applications
 
   An application can be provided with a name as an option to `start_link/1`.
@@ -105,7 +114,6 @@ defmodule Commanded.Application do
 
       :ok = MyApp.Application.dispatch(command, application: :tenant1)
 
-
   ## Default dispatch options
 
   An application can be configured with default command dispatch options such as
@@ -219,15 +227,97 @@ defmodule Commanded.Application do
 
   @doc """
   Dispatch a registered command.
+
+    - `command` is a command struct which must be registered with a
+      `Commanded.Commands.Router` and included in the application.
+      
+  """
+  @callback dispatch(command :: struct()) ::
+              :ok
+              | {:ok, aggregate_state :: struct()}
+              | {:ok, aggregate_version :: non_neg_integer()}
+              | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
+              | {:error, :unregistered_command}
+              | {:error, :consistency_timeout}
+              | {:error, reason :: term()}
+
+  @doc """
+  Dispatch a registered command.
+
+    - `command` is a command struct which must be registered with a
+      `Commanded.Commands.Router` and included in the application.
+
+    - `timeout_or_opts` is either an integer timeout or a keyword list of
+      options.
+
+      The timeout must be an integer greater than zero which specifies how many
+      milliseconds to allow the command to be handled, or the atom `:infinity`
+      to wait indefinitely. The default timeout value is five seconds.
+
+      Alternatively, an options keyword list can be provided, it supports the
+      following options.
+
+      Options:
+
+        - `causation_id` - an optional UUID used to identify the cause of the
+          command being dispatched.
+
+        - `correlation_id` - an optional UUID used to correlate related
+          commands/events together.
+
+        - `consistency` - one of `:eventual` (default) or `:strong`. By
+          setting the consistency to `:strong` a successful command dispatch
+          will block until all strongly consistent event handlers and process
+          managers have handled all events created by the command.
+
+        - `metadata` - an optional map containing key/value pairs comprising
+          the metadata to be associated with all events created by the
+          command.
+
+        - `returning` - to choose what response is returned from a successful
+            command dispatch. The default is to return an `:ok`.
+
+            The available options are:
+
+            - `:aggregate_state` - to return the update aggregate state in the
+              successful response: `{:ok, aggregate_state}`.
+
+            - `:aggregate_version` - to include the aggregate stream version
+              in the successful response: `{:ok, aggregate_version}`.
+
+            - `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
+              struct containing the aggregate's identity, version, and any
+              events produced from the command along with their associated
+              metadata.
+
+            - `false` - don't return anything except an `:ok`.
+
+        - `timeout` - as described above.
+
+  Returns `:ok` on success unless the `:returning` option is specified where
+  it returns one of `{:ok, aggregate_state}`, `{:ok, aggregate_version}`, or
+  `{:ok, %Commanded.Commands.ExecutionResult{}}`.
+
+  Returns `{:error, reason}` on failure.
+
+  ## Example
+
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
+
+      :ok = BankApp.dispatch(command, timeout: 30_000)
+
   """
-  @callback dispatch(command :: struct, timeout_or_opts :: integer | :infinity | Keyword.t()) ::
+  @callback dispatch(
+              command :: struct(),
+              timeout_or_opts :: non_neg_integer() | :infinity | Keyword.t()
+            ) ::
               :ok
-              | {:ok, aggregate_state :: struct}
+              | {:ok, aggregate_state :: struct()}
               | {:ok, aggregate_version :: non_neg_integer()}
               | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
-              | {:error, reason :: term}
+              | {:error, reason :: term()}
 
   alias Commanded.Application.Config
 
diff --git a/lib/commanded/commands/composite_router.ex b/lib/commanded/commands/composite_router.ex
index bbd8854..7519fc4 100644
--- a/lib/commanded/commands/composite_router.ex
+++ b/lib/commanded/commands/composite_router.ex
@@ -19,14 +19,25 @@ defmodule Commanded.Commands.CompositeRouter do
         router(Bank.MoneyTransfer.Router)
       end
 
-  You can dispatch a command via the composite router which will be routed to
-  the associated router:
+    One or more routers or composite routers can be included in a
+    `Commanded.Application`  since it is also a composite router:
 
-      alias Bank.AppRouter
+      defmodule BankApp do
+        use Commanded.Application
+
+        router(Bank.AppRouter)
+      end
+
+  You can dispatch a command via the application which will then be routed to
+  the associated child router:
 
       command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
 
-      :ok = AppRouter.dispatch(command)
+      :ok = BankApp.dispatch(command)
+
+  Or via the composite router itself, specifying the application:
+
+      :ok = Bank.AppRouter.dispatch(command, application: BankApp)
 
   A composite router can include composite routers.
   """
@@ -37,6 +48,10 @@ defmodule Commanded.Commands.CompositeRouter do
 
       import unquote(__MODULE__)
 
+      @before_compile unquote(__MODULE__)
+
+      Module.register_attribute(__MODULE__, :registered_commands, accumulate: false)
+
       application = Keyword.get(unquote(opts), :application)
 
       default_dispatch_opts =
@@ -46,11 +61,15 @@ defmodule Commanded.Commands.CompositeRouter do
 
       @default_dispatch_opts default_dispatch_opts
       @registered_commands %{}
-
-      @before_compile unquote(__MODULE__)
     end
   end
 
+  @doc """
+  Register a `Commanded.Commands.Router` module within this composite router.
+
+  Will allow the composite router to dispatch any commands registered by the
+  included router module. Multiple routers can be registered.
+  """
   defmacro router(router_module) do
     quote location: :keep do
       for command <- unquote(router_module).__registered_commands__() do
@@ -71,47 +90,37 @@ defmodule Commanded.Commands.CompositeRouter do
     quote generated: true do
       @doc false
       def __registered_commands__ do
-        Enum.map(@registered_commands, fn {command, _router} -> command end)
-      end
-
-      def __dispatch_opts__(opts) do
-        Keyword.merge(@default_dispatch_opts, opts)
+        Enum.map(@registered_commands, fn {command_module, _router} -> command_module end)
       end
 
       @doc false
       def dispatch(command, opts \\ [])
 
-      Enum.map(@registered_commands, fn {command_module, router} ->
-        Module.eval_quoted(
-          __MODULE__,
-          quote do
-            @doc false
-            def dispatch(%unquote(command_module){} = command, :infinity) do
-              opts = __dispatch_opts__(timeout: :infinity)
+      @doc false
+      def dispatch(command, :infinity),
+        do: do_dispatch(command, timeout: :infinity)
 
-              unquote(router).dispatch(command, opts)
-            end
+      @doc false
+      def dispatch(command, timeout) when is_integer(timeout),
+        do: do_dispatch(command, timeout: timeout)
 
-            @doc false
-            def dispatch(%unquote(command_module){} = command, timeout)
-                when is_integer(timeout) do
-              opts = __dispatch_opts__(timeout: timeout)
+      @doc false
+      def dispatch(command, opts),
+        do: do_dispatch(command, opts)
 
-              unquote(router).dispatch(command, opts)
-            end
+      for {command_module, router} <- @registered_commands do
+        @command_module command_module
+        @router router
 
-            @doc false
-            def dispatch(%unquote(command_module){} = command, opts) do
-              opts = __dispatch_opts__(opts)
+        defp do_dispatch(%@command_module{} = command, opts) do
+          opts = Keyword.merge(@default_dispatch_opts, opts)
 
-              unquote(router).dispatch(command, opts)
-            end
-          end
-        )
-      end)
+          @router.dispatch(command, opts)
+        end
+      end
 
-      @doc false
-      def dispatch(command, _opts) do
+      # Catch unregistered commands, log and return an error.
+      defp do_dispatch(command, _opts) do
         Logger.error(fn ->
           "attempted to dispatch an unregistered command: " <> inspect(command)
         end)
diff --git a/lib/commanded/commands/handler.ex b/lib/commanded/commands/handler.ex
index d3bc88d..7161d96 100644
--- a/lib/commanded/commands/handler.ex
+++ b/lib/commanded/commands/handler.ex
@@ -9,7 +9,9 @@ defmodule Commanded.Commands.Handler do
       defmodule OpenAccountHandler do
         @behaviour Commanded.Commands.Handler
 
-        def handle(%BankAccount{} = aggregate, %OpenAccount{account_number: account_number, initial_balance: initial_balance}) do
+        def handle(%BankAccount{} = aggregate, %OpenAccount{} = command) do
+          %OpenAccount{account_number: account_number, initial_balance: initial_balance} = command
+
           BankAccount.open_account(aggregate, account_number, initial_balance)
         end
       end
@@ -18,15 +20,22 @@ defmodule Commanded.Commands.Handler do
   @type aggregate :: struct()
   @type command :: struct()
   @type domain_event :: struct
-  @type domain_events :: list(struct())
-  @type reason :: term()
+  @type reason :: any()
 
   @doc """
   Apply the given command to the event sourced aggregate.
 
-  You must return a list containing the pending events, or `nil` / `[]` when no events produced.
+  You must return a single domain event, a list containing the pending events,
+  or `nil`, `[]`, or `:ok` when no events are produced.
 
   You should return `{:error, reason}` on failure.
   """
-  @callback handle(aggregate, command) :: domain_event | domain_events | nil | {:error, reason}
+  @callback handle(aggregate, command) ::
+              domain_event
+              | list(domain_event)
+              | {:ok, domain_event}
+              | {:ok, list(domain_event)}
+              | :ok
+              | nil
+              | {:error, reason}
 end
diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex
index c87fabb..e1fd340 100644
--- a/lib/commanded/commands/router.ex
+++ b/lib/commanded/commands/router.ex
@@ -203,33 +203,37 @@ defmodule Commanded.Commands.Router do
 
   """
 
-  @callback dispatch(struct, keyword()) ::
-              :ok | {:ok, non_neg_integer()} | {:ok, struct} | {:error, term}
+  alias Commanded.Commands.Router
 
   defmacro __using__(opts) do
     quote do
       require Logger
 
       import unquote(__MODULE__)
+
       @before_compile unquote(__MODULE__)
+      @behaviour Commanded.Commands.Router
 
-      @application Keyword.get(unquote(opts), :application)
-      @registered_commands []
-      @registered_identities %{}
-      @registered_middleware []
-
-      @default [
-        middleware: [
-          Commanded.Middleware.ExtractAggregateIdentity,
-          Commanded.Middleware.ConsistencyGuarantee
-        ],
-        consistency: get_opt(unquote(opts), :default_consistency, :eventual),
-        returning: get_default_dispatch_return(unquote(opts)),
-        dispatch_timeout: 5_000,
+      Module.register_attribute(__MODULE__, :registered_commands, accumulate: true)
+      Module.register_attribute(__MODULE__, :registered_middleware, accumulate: true)
+      Module.register_attribute(__MODULE__, :registered_identities, accumulate: false)
+
+      @default_dispatch_opts [
+        application: Keyword.get(unquote(opts), :application),
+        consistency: Router.get_opt(unquote(opts), :default_consistency, :eventual),
+        returning: Router.get_default_dispatch_return(unquote(opts)),
+        timeout: 5_000,
         lifespan: Commanded.Aggregates.DefaultLifespan,
         metadata: %{},
         retry_attempts: 10
       ]
+
+      @default_middleware [
+        Commanded.Middleware.ExtractAggregateIdentity,
+        Commanded.Middleware.ConsistencyGuarantee
+      ]
+
+      @registered_identities %{}
     end
   end
 
@@ -256,7 +260,7 @@ defmodule Commanded.Commands.Router do
   """
   defmacro middleware(middleware_module) do
     quote do
-      @registered_middleware @registered_middleware ++ [unquote(middleware_module)]
+      @registered_middleware unquote(middleware_module)
     end
   end
 
@@ -331,7 +335,7 @@ defmodule Commanded.Commands.Router do
 
   @doc """
   Configure the command, or list of commands, to be dispatched to the
-  corresponding handler for a given aggregate.
+  corresponding handler and aggregate.
 
   ## Example
 
@@ -345,290 +349,239 @@ defmodule Commanded.Commands.Router do
   defmacro dispatch(command_module_or_modules, opts) do
     opts = parse_opts(opts, [])
 
-    command_module_or_modules
-    |> List.wrap()
-    |> Enum.map(fn command_module ->
+    for command_module <- List.wrap(command_module_or_modules) do
       quote do
-        register(unquote(command_module), unquote(opts))
-      end
-    end)
-  end
-
-  @register_params [
-    :to,
-    :function,
-    :aggregate,
-    :identity,
-    :identity_prefix,
-    :timeout,
-    :lifespan,
-    :consistency
-  ]
-
-  @doc false
-  defmacro register(command_module,
-             to: handler,
-             function: function,
-             aggregate: aggregate,
-             identity: identity,
-             identity_prefix: identity_prefix,
-             timeout: timeout,
-             lifespan: lifespan,
-             consistency: consistency
-           ) do
-    quote location: :keep do
-      if Enum.member?(@registered_commands, unquote(command_module)) do
-        raise ArgumentError,
-          message:
-            "Command `#{inspect(unquote(command_module))}` has already been registered in router `#{
-              inspect(__MODULE__)
-            }`"
-      end
-
-      # sanity check the configured modules exist
-      ensure_module_exists(unquote(aggregate))
-      ensure_module_exists(unquote(command_module))
-      ensure_module_exists(unquote(handler))
-
-      case unquote(lifespan) do
-        nil ->
-          :ok
-
-        module when is_atom(module) ->
-          ensure_module_exists(unquote(lifespan))
-
-          unless function_exported?(unquote(lifespan), :after_event, 1) do
-            raise ArgumentError,
-              message:
-                "Aggregate lifespan `#{inspect(unquote(lifespan))}` does not define a callback function: `after_event/1`"
-          end
-
-          unless function_exported?(unquote(lifespan), :after_command, 1) do
-            raise ArgumentError,
-              message:
-                "Aggregate lifespan `#{inspect(unquote(lifespan))}` does not define a callback function: `after_command/1`"
-          end
-
-          unless function_exported?(unquote(lifespan), :after_error, 1) do
-            raise ArgumentError,
-              message:
-                "Aggregate lifespan `#{inspect(unquote(lifespan))}` does not define a callback function: `after_error/1`"
-          end
-
-        invalid ->
+        if Enum.any?(@registered_commands, fn {command_module, _command_opts} ->
+             command_module == unquote(command_module)
+           end) do
           raise ArgumentError,
             message:
-              "Invalid `lifespan` configured for #{inspect(unquote(aggregate))}: " <>
-                inspect(invalid)
-      end
-
-      unless function_exported?(unquote(handler), unquote(function), 2) do
-        raise ArgumentError,
-          message:
-            "Command handler `#{inspect(unquote(handler))}` does not define a `#{
-              unquote(function)
-            }/2` function"
-      end
-
-      @registered_commands [unquote(command_module) | @registered_commands]
-
-      def dispatch(command)
-      def dispatch(%unquote(command_module){} = command), do: do_dispatch(command, [])
-
-      def dispatch(command, timeout_or_opts)
-
-      def dispatch(%unquote(command_module){} = command, :infinity),
-        do: do_dispatch(command, timeout: :infinity)
-
-      def dispatch(%unquote(command_module){} = command, timeout) when is_integer(timeout),
-        do: do_dispatch(command, timeout: timeout)
-
-      def dispatch(%unquote(command_module){} = command, opts),
-        do: do_dispatch(command, opts)
-
-      defp do_dispatch(%unquote(command_module){} = command, opts) do
-        unless application = Keyword.get(opts, :application, @application) do
-          raise ArgumentError, message: "no :application provided"
+              "Command `#{inspect(unquote(command_module))}` has already been registered in router `#{
+                inspect(__MODULE__)
+              }`"
         end
 
-        causation_id = Keyword.get(opts, :causation_id)
-        correlation_id = Keyword.get(opts, :correlation_id) || UUID.uuid4()
-
-        consistency =
-          Keyword.get(opts, :consistency) || unquote(consistency) || @default[:consistency]
-
-        metadata = Keyword.get(opts, :metadata) || @default[:metadata]
-        timeout = Keyword.get(opts, :timeout) || unquote(timeout) || @default[:dispatch_timeout]
-
-        returning =
-          cond do
-            (returning = Keyword.get(opts, :returning)) in [
-              :aggregate_state,
-              :aggregate_version,
-              :execution_result,
-              false
-            ] ->
-              returning
-
-            Keyword.get(opts, :include_execution_result) == true ->
-              :execution_result
-
-            Keyword.get(opts, :include_aggregate_version) == true ->
-              :aggregate_version
-
-            true ->
-              @default[:returning]
-          end
-
-        lifespan = Keyword.get(opts, :lifespan) || unquote(lifespan) || @default[:lifespan]
-        retry_attempts = Keyword.get(opts, :retry_attempts) || @default[:retry_attempts]
-
-        {identity, identity_prefix} =
-          case Map.get(@registered_identities, unquote(aggregate)) do
-            nil ->
-              {unquote(identity), unquote(identity_prefix)}
-
-            config ->
-              identity = Keyword.get(config, :by) || unquote(identity)
-              prefix = Keyword.get(config, :prefix) || unquote(identity_prefix)
-
-              {identity, prefix}
-          end
-
-        alias Commanded.Commands.Dispatcher
-        alias Commanded.Commands.Dispatcher.Payload
-
-        payload = %Payload{
-          application: application,
-          command: command,
-          command_uuid: UUID.uuid4(),
-          causation_id: causation_id,
-          correlation_id: correlation_id,
-          consistency: consistency,
-          handler_module: unquote(handler),
-          handler_function: unquote(function),
-          aggregate_module: unquote(aggregate),
-          identity: identity,
-          identity_prefix: identity_prefix,
-          returning: returning,
-          timeout: timeout,
-          lifespan: lifespan,
-          metadata: metadata,
-          middleware: @registered_middleware ++ @default[:middleware],
-          retry_attempts: retry_attempts
+        @registered_commands {
+          unquote(command_module),
+          Keyword.merge(@default_dispatch_opts, unquote(opts))
         }
-
-        Dispatcher.dispatch(payload)
       end
     end
   end
 
-  defmacro __before_compile__(_env) do
-    quote generated: true do
-      @doc false
-      def __registered_commands__, do: @registered_commands
+  @doc """
+  Dispatch the given command to the registered handler.
+
+  Returns `:ok` on success, or `{:error, reason}` on failure.
 
-      @doc """
-      Dispatch the given command to the registered handler.
+  ## Example
+
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
 
-      Returns `:ok` on success, or `{:error, reason}` on failure.
-      """
-      @spec dispatch(command :: struct) ::
+      :ok = BankRouter.dispatch(command)
+
+  """
+  @callback dispatch(command :: struct()) ::
               :ok
-              | {:ok, aggregate_state :: struct}
+              | {:ok, aggregate_state :: struct()}
               | {:ok, aggregate_version :: non_neg_integer()}
               | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
-              | {:error, reason :: term}
-      def dispatch(command), do: unregistered_command(command)
+              | {:error, reason :: term()}
+
+  @doc """
+  Dispatch the given command to the registered handler providing a timeout.
+
+    - `command` is a command struct which must be registered with the router.
+
+    - `timeout_or_opts` is either an integer timeout, `:infinity`, or a keyword
+      list of options.
+
+      The timeout must be an integer greater than zero which specifies how many
+      milliseconds to allow the command to be handled, or the atom `:infinity`
+      to wait indefinitely. The default timeout value is five seconds.
 
-      @doc """
-      Dispatch the given command to the registered handler providing a timeout.
+      Alternatively, an options keyword list can be provided with the following
+      options.
 
-      - `timeout_or_opts` is either an integer timeout or a keyword list of
-        options. The timeout must be an integer greater than zero which
-        specifies how many milliseconds to allow the command to be handled, or
-        the atom `:infinity` to wait indefinitely. The default timeout value is
-        five seconds.
+      Options:
 
-        Alternatively, an options keyword list can be provided, it supports the
-        following options.
+        - `causation_id` - an optional UUID used to identify the cause of the
+          command being dispatched.
 
-        Options:
+        - `correlation_id` - an optional UUID used to correlate related
+          commands/events together.
 
-          - `causation_id` - an optional UUID used to identify the cause of the
-            command being dispatched.
+        - `consistency` - one of `:eventual` (default) or `:strong`. By
+          setting the consistency to `:strong` a successful command dispatch
+          will block until all strongly consistent event handlers and process
+          managers have handled all events created by the command.
 
-          - `correlation_id` - an optional UUID used to correlate related
-            commands/events together.
+        - `metadata` - an optional map containing key/value pairs comprising
+          the metadata to be associated with all events created by the
+          command.
 
-          - `consistency` - one of `:eventual` (default) or `:strong`. By
-            setting the consistency to `:strong` a successful command dispatch
-            will block until all strongly consistent event handlers and process
-            managers have handled all events created by the command.
+        - `returning` - to choose what response is returned from a successful
+            command dispatch. The default is to return an `:ok`.
 
-          - `metadata` - an optional map containing key/value pairs comprising
-            the metadata to be associated with all events created by the
-            command.
+            The available options are:
 
-          - `returning` - to choose what response is returned from a successful
-              command dispatch. The default is to return an `:ok`.
+            - `:aggregate_state` - to return the update aggregate state in the
+              successful response: `{:ok, aggregate_state}`.
 
-              The available options are:
+            - `:aggregate_version` - to include the aggregate stream version
+              in the successful response: `{:ok, aggregate_version}`.
 
-              - `:aggregate_state` - to return the update aggregate state in the
-                successful response: `{:ok, aggregate_state}`.
+            - `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
+              struct containing the aggregate's identity, version, and any
+              events produced from the command along with their associated
+              metadata.
 
-              - `:aggregate_version` - to include the aggregate stream version
-                in the successful response: `{:ok, aggregate_version}`.
+            - `false` - don't return anything except an `:ok`.
 
-              - `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
-                struct containing the aggregate's identity, version, and any
-                events produced from the command along with their associated
-                metadata.
+        - `timeout` - as described above.
 
-              - `false` - don't return anything except an `:ok`.
+  Returns `:ok` on success unless the `:returning` option is specified where
+  it returns one of `{:ok, aggregate_state}`, `{:ok, aggregate_version}`, or
+  `{:ok, %Commanded.Commands.ExecutionResult{}}`.
 
-          - `timeout` - as described above.
+  Returns `{:error, reason}` on failure.
 
-      Returns `:ok` on success unless the `:returning` option is specified where
-      it returns one of `{:ok, aggregate_state}`, `{:ok, aggregate_version}`, or
-      `{:ok, %Commanded.Commands.ExecutionResult{}}`.
+  ## Example
+
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
 
-      Returns `{:error, reason}` on failure.
-      """
-      @spec dispatch(command :: struct, timeout_or_opts :: integer | :infinity | keyword()) ::
+      :ok = BankRouter.dispatch(command, consistency: :strong, timeout: 30_000)
+
+  """
+  @callback dispatch(
+              command :: struct(),
+              timeout_or_opts :: non_neg_integer() | :infinity | Keyword.t()
+            ) ::
               :ok
-              | {:ok, aggregate_state :: struct}
+              | {:ok, aggregate_state :: struct()}
               | {:ok, aggregate_version :: non_neg_integer()}
               | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
-              | {:error, reason :: term}
-      def dispatch(command, _opts), do: unregistered_command(command)
+              | {:error, reason :: term()}
+
+  defmacro __before_compile__(_env) do
+    quote generated: true do
+      @doc false
+      def __registered_commands__ do
+        Enum.map(@registered_commands, fn {command_module, _command_opts} -> command_module end)
+      end
 
-      defp unregistered_command(command) do
-        _ =
-          Logger.error(fn ->
-            "attempted to dispatch an unregistered command: " <> inspect(command)
-          end)
+      @doc false
+      def dispatch(command, opts \\ [])
 
-        {:error, :unregistered_command}
+      @doc false
+      def dispatch(command, :infinity),
+        do: do_dispatch(command, timeout: :infinity)
+
+      @doc false
+      def dispatch(command, timeout) when is_integer(timeout),
+        do: do_dispatch(command, timeout: timeout)
+
+      @doc false
+      def dispatch(command, opts),
+        do: do_dispatch(command, opts)
+
+      @middleware Enum.reduce(@registered_middleware, @default_middleware, fn middleware, acc ->
+                    [middleware | acc]
+                  end)
+
+      for {command_module, command_opts} <- @registered_commands do
+        @aggregate Keyword.fetch!(command_opts, :aggregate)
+        @handler Keyword.fetch!(command_opts, :to)
+        @function Keyword.fetch!(command_opts, :function)
+        @lifespan Keyword.get(command_opts, :lifespan)
+        @identity Keyword.get(command_opts, :identity)
+        @identity_prefix Keyword.get(command_opts, :identity_prefix)
+
+        @command_module command_module
+        @command_opts command_opts
+
+        defp do_dispatch(%@command_module{} = command, opts) do
+          alias Commanded.Commands.Dispatcher
+          alias Commanded.Commands.Dispatcher.Payload
+
+          opts = Keyword.merge(@command_opts, opts)
+
+          application = Keyword.fetch!(opts, :application)
+          causation_id = Keyword.get(opts, :causation_id)
+          correlation_id = Keyword.get(opts, :correlation_id, UUID.uuid4())
+          consistency = Keyword.fetch!(opts, :consistency)
+          metadata = Keyword.fetch!(opts, :metadata)
+          retry_attempts = Keyword.get(opts, :retry_attempts)
+          timeout = Keyword.fetch!(opts, :timeout)
+
+          returning =
+            cond do
+              Keyword.get(opts, :include_execution_result) == true ->
+                :execution_result
+
+              Keyword.get(opts, :include_aggregate_version) == true ->
+                :aggregate_version
+
+              (returning = Keyword.get(opts, :returning)) in [
+                :aggregate_state,
+                :aggregate_version,
+                :execution_result,
+                false
+              ] ->
+                returning
+
+              true ->
+                false
+            end
+
+          {identity, identity_prefix} =
+            case Map.get(@registered_identities, @aggregate) do
+              nil ->
+                {@identity, @identity_prefix}
+
+              config ->
+                identity = Keyword.get(config, :by, @identity)
+                prefix = Keyword.get(config, :prefix, @identity_prefix)
+
+                {identity, prefix}
+            end
+
+          payload = %Payload{
+            application: application,
+            command: command,
+            command_uuid: UUID.uuid4(),
+            causation_id: causation_id,
+            correlation_id: correlation_id,
+            consistency: consistency,
+            handler_module: @handler,
+            handler_function: @function,
+            aggregate_module: @aggregate,
+            identity: identity,
+            identity_prefix: identity_prefix,
+            returning: returning,
+            timeout: timeout,
+            lifespan: @lifespan,
+            metadata: metadata,
+            middleware: @middleware,
+            retry_attempts: retry_attempts
+          }
+
+          Dispatcher.dispatch(payload)
+        end
       end
-    end
-  end
 
-  @doc false
-  def ensure_module_exists(module) do
-    case Code.ensure_compiled(module) do
-      {:module, ^module} ->
-        :ok
-
-      {:error, _error} ->
-        raise ArgumentError,
-          message:
-            "module `#{inspect(module)}` does not exist, perhaps you forgot to `alias` the namespace"
+      # Catch unregistered commands, log and return an error.
+      defp do_dispatch(command, _opts) do
+        Logger.error(fn ->
+          "attempted to dispatch an unregistered command: " <> inspect(command)
+        end)
+
+        {:error, :unregistered_command}
+      end
     end
   end
 
@@ -659,6 +612,17 @@ defmodule Commanded.Commands.Router do
     end
   end
 
+  @register_params [
+    :to,
+    :function,
+    :aggregate,
+    :identity,
+    :identity_prefix,
+    :timeout,
+    :lifespan,
+    :consistency
+  ]
+
   defp parse_opts([{:to, aggregate_or_handler} | opts], result) do
     case Keyword.pop(opts, :aggregate) do
       {nil, opts} ->
@@ -682,7 +646,5 @@ defmodule Commanded.Commands.Router do
     """
   end
 
-  defp parse_opts([], result) do
-    Enum.map(@register_params, fn key -> {key, Keyword.get(result, key)} end)
-  end
+  defp parse_opts([], result), do: result
 end


Test patch:
diff --git a/test/aggregates/aggregate_lifespan_test.exs b/test/aggregates/aggregate_lifespan_test.exs
index 5bf191f..85ea99d 100644
--- a/test/aggregates/aggregate_lifespan_test.exs
+++ b/test/aggregates/aggregate_lifespan_test.exs
@@ -321,53 +321,4 @@ defmodule Commanded.Aggregates.AggregateLifespanTest do
       refute_receive {:DOWN, ^ref, :process, ^pid, _}
     end
   end
-
-  describe "compile time safety" do
-    test "should fail to compile when missing `after_event/1` function" do
-      assert_raise ArgumentError,
-                   "Aggregate lifespan `BankAccountLifespan1` does not define a callback function: `after_event/1`",
-                   fn ->
-                     Code.eval_string("""
-                       alias Commanded.ExampleDomain.BankAccount
-                       alias Commanded.ExampleDomain.BankAccount.Commands.{OpenAccount, DepositMoney}
-
-                       defmodule BankAccountLifespan1 do
-                       end
-
-                       defmodule BankRouter do
-                         use Commanded.Commands.Router
-
-                         dispatch [OpenAccount],
-                           to: BankAccount,
-                           lifespan: BankAccountLifespan1,
-                           identity: :account_number
-                       end
-                     """)
-                   end
-    end
-
-    test "should fail to compile when missing `after_command/1` function" do
-      assert_raise ArgumentError,
-                   "Aggregate lifespan `BankAccountLifespan2` does not define a callback function: `after_command/1`",
-                   fn ->
-                     Code.eval_string("""
-                       alias Commanded.ExampleDomain.BankAccount
-                       alias Commanded.ExampleDomain.BankAccount.Commands.{OpenAccount, DepositMoney}
-
-                       defmodule BankAccountLifespan2 do
-                         def after_event(_event), do: :stop
-                       end
-
-                       defmodule BankRouter do
-                         use Commanded.Commands.Router
-
-                         dispatch [OpenAccount],
-                           to: BankAccount,
-                           lifespan: BankAccountLifespan2,
-                           identity: :account_number
-                       end
-                     """)
-                   end
-    end
-  end
 end
diff --git a/test/commands/correlation_causation_test.exs b/test/commands/correlation_causation_test.exs
index 2bb6b2b..f560d8d 100644
--- a/test/commands/correlation_causation_test.exs
+++ b/test/commands/correlation_causation_test.exs
@@ -98,8 +98,7 @@ defmodule Commanded.Commands.CorrelationCasuationTest do
       :ok = BankRouter.dispatch(command, application: BankApp, correlation_id: nil)
 
       assert [correlation_id] = CommandAuditMiddleware.dispatched_commands(& &1.correlation_id)
-      refute is_nil(correlation_id)
-      assert correlation_id |> UUID.string_to_binary!() |> is_binary()
+      assert is_nil(correlation_id)
     end
 
     test "should be copied to dispatched command" do
diff --git a/test/commands/routing_commands_test.exs b/test/commands/routing_commands_test.exs
index ec55b43..d672c06 100644
--- a/test/commands/routing_commands_test.exs
+++ b/test/commands/routing_commands_test.exs
@@ -216,26 +216,6 @@ defmodule Commanded.Commands.RoutingCommandsTest do
                  end
   end
 
-  test "should prevent registration for a command handler without a `handle/2` function" do
-    assert_raise ArgumentError,
-                 "Command handler `InvalidHandler` does not define a `handle/2` function",
-                 fn ->
-                   Code.eval_string("""
-                     alias Commanded.ExampleDomain.BankAccount
-                     alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
-
-                     defmodule InvalidHandler do
-                     end
-
-                     defmodule InvalidRouter do
-                       use Commanded.Commands.Router
-
-                       dispatch OpenAccount, to: InvalidHandler, aggregate: BankAccount, identity: :account_number
-                     end
-                   """)
-                 end
-  end
-
   test "should show a helpful message when bad argument given to a `dispatch/2` function" do
     assert_raise RuntimeError,
                  """
@@ -256,57 +236,6 @@ defmodule Commanded.Commands.RoutingCommandsTest do
                  end
   end
 
-  test "should prevent registrations for a invalid command module" do
-    assert_raise ArgumentError,
-                 "module `UnknownCommand` does not exist, perhaps you forgot to `alias` the namespace",
-                 fn ->
-                   Code.eval_string("""
-                     alias Commanded.ExampleDomain.BankAccount
-                     alias Commanded.ExampleDomain.OpenAccountHandler
-
-                     defmodule InvalidCommandRouter do
-                       use Commanded.Commands.Router
-
-                       dispatch UnknownCommand, to: OpenAccountHandler, aggregate: BankAccount, identity: :account_number
-                     end
-                   """)
-                 end
-  end
-
-  test "should prevent registrations for an invalid command handler module" do
-    assert_raise ArgumentError,
-                 "module `UnknownHandler` does not exist, perhaps you forgot to `alias` the namespace",
-                 fn ->
-                   Code.eval_string("""
-                     alias Commanded.ExampleDomain.BankAccount
-                     alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
-
-                     defmodule InvalidHandlerRouter do
-                       use Commanded.Commands.Router
-
-                       dispatch OpenAccount, to: UnknownHandler, aggregate: BankAccount, identity: :account_number
-                     end
-                   """)
-                 end
-  end
-
-  test "should prevent registrations for an invalid aggregate module" do
-    assert_raise ArgumentError,
-                 "module `UnknownAggregate` does not exist, perhaps you forgot to `alias` the namespace",
-                 fn ->
-                   Code.eval_string("""
-                     alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
-                     alias Commanded.ExampleDomain.OpenAccountHandler
-
-                     defmodule InvalidAggregateRouter do
-                       use Commanded.Commands.Router
-
-                       dispatch OpenAccount, to: OpenAccountHandler, aggregate: UnknownAggregate, identity: :account_number
-                     end
-                   """)
-                 end
-  end
-
   defmodule MultiCommandRouter do
     use Commanded.Commands.Router
 
diff --git a/test/commands/support/compilation/example_aggregate.ex b/test/commands/support/compilation/example_aggregate.ex
new file mode 100644
index 0000000..131577d
--- /dev/null
+++ b/test/commands/support/compilation/example_aggregate.ex
@@ -0,0 +1,8 @@
+defmodule Commanded.Commands.ExampleAggregate do
+  alias Commanded.Commands.ExampleAggregate
+  alias Commanded.Commands.ExampleCommand
+
+  defstruct [:id]
+
+  def execute(%ExampleAggregate{}, %ExampleCommand{}), do: []
+end
diff --git a/test/commands/support/compilation/example_command.ex b/test/commands/support/compilation/example_command.ex
new file mode 100644
index 0000000..f285529
--- /dev/null
+++ b/test/commands/support/compilation/example_command.ex
@@ -0,0 +1,3 @@
+defmodule Commanded.Commands.ExampleCommand do
+  defstruct [:id]
+end
diff --git a/test/commands/support/compilation/example_router.ex b/test/commands/support/compilation/example_router.ex
new file mode 100644
index 0000000..10cc622
--- /dev/null
+++ b/test/commands/support/compilation/example_router.ex
@@ -0,0 +1,8 @@
+defmodule Commanded.Commands.ExampleRouter do
+  use Commanded.Commands.Router
+
+  alias Commanded.Commands.ExampleCommand
+  alias Commanded.Commands.ExampleAggregate
+
+  dispatch ExampleCommand, to: ExampleAggregate, identity: :id
+end
diff --git a/test/support/mocked_app.ex b/test/support/mocked_app.ex
index 6f39b17..d2af413 100644
--- a/test/support/mocked_app.ex
+++ b/test/support/mocked_app.ex
@@ -8,6 +8,4 @@ defmodule Commanded.MockedApp do
     ],
     registry: :local,
     pubsub: :local
-
-  # router(Commanded.Commands.MockRouter)
 end