Repository: commanded/commanded
Pull request #352 (2020-03-09 12:57:15)

Allow Commanded Application name to be set dynamically in middleware
Allow setting the Commanded Application name in router middleware to support dynamic applications, such as using tenant-specific a app and event store.

### Example

```elixir
defmodule TenantMiddleware do
  @behaviour Commanded.Middleware

  alias Commanded.Middleware.Pipeline

  def before_dispatch(%Pipeline{} = pipeline) do
    %Pipeline{command: command} = pipeline

    # Dynamically set application name from `tenant_id` in command
    tenant_id = Map.fetch!(command, :tenant_id)
    application = Module.concat([TenantApp, "tenant#{tenant_id}"])

    %Pipeline{pipeline | application: application}
  end

  def after_dispatch(pipeline), do: pipeline
  def after_failure(pipeline), do: pipeline
end
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f7fa16..5a5a5af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 - Run the formatter in CI ([#341](https://github.com/commanded/commanded/pull/341)).
 - Add stacktraces to EventHandler error logging ([#340](https://github.com/commanded/commanded/pull/340))
 - `refute_receive_event/4` only tests newly created events ([#347](https://github.com/commanded/commanded/pull/347)).
+- Allow Commanded Application name to be set dynamically in middleware ([#352](https://github.com/commanded/commanded/pull/352)).
 
 ### Bug fixes
 
diff --git a/lib/commanded/commands/dispatcher.ex b/lib/commanded/commands/dispatcher.ex
index eecb725..25ef902 100644
--- a/lib/commanded/commands/dispatcher.ex
+++ b/lib/commanded/commands/dispatcher.ex
@@ -62,14 +62,13 @@ defmodule Commanded.Commands.Dispatcher do
     end
   end
 
-  defp to_pipeline(%Payload{} = payload),
-    do: struct(Pipeline, Map.from_struct(payload))
+  defp to_pipeline(%Payload{} = payload) do
+    struct(Pipeline, Map.from_struct(payload))
+  end
 
   defp execute(%Pipeline{} = pipeline, %Payload{} = payload, %ExecutionContext{} = context) do
-    %Pipeline{assigns: %{aggregate_uuid: aggregate_uuid}} = pipeline
-
-    %Payload{application: application, aggregate_module: aggregate_module, timeout: timeout} =
-      payload
+    %Pipeline{application: application, assigns: %{aggregate_uuid: aggregate_uuid}} = pipeline
+    %Payload{aggregate_module: aggregate_module, timeout: timeout} = payload
 
     {:ok, ^aggregate_uuid} =
       Commanded.Aggregates.Supervisor.open_aggregate(


Test patch:
diff --git a/config/test.exs b/config/test.exs
index 87afc8d..fb35a20 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -23,6 +23,7 @@ default_app_config = [
 config :commanded, Commanded.Commands.ConsistencyApp, default_app_config
 config :commanded, Commanded.DefaultApp, default_app_config
 config :commanded, Commanded.Event.Upcast.ProcessManager.Application, default_app_config
+config :commanded, Commanded.Middleware.TenantApp, default_app_config
 config :commanded, Commanded.ProcessManagers.ErrorApp, default_app_config
 config :commanded, Commanded.ProcessManagers.ExampleApp, default_app_config
 config :commanded, Commanded.ProcessManagers.ResumeApp, default_app_config
diff --git a/test/middleware/application_middleware_test.exs b/test/middleware/application_middleware_test.exs
new file mode 100644
index 0000000..705fa34
--- /dev/null
+++ b/test/middleware/application_middleware_test.exs
@@ -0,0 +1,34 @@
+defmodule Commanded.Middleware.ApplicationMiddlewareTest do
+  use Commanded.StorageCase
+
+  alias Commanded.Middleware.Tenant.Commands.RegisterTenant
+  alias Commanded.Middleware.TenantApp
+
+  setup do
+    for tenant_id <- 1..3 do
+      start_supervised!({TenantApp, name: TenantApp.tenant_application_name(tenant_id)})
+    end
+
+    :ok
+  end
+
+  test "dispatch command to dynamic application set in middleware" do
+    for tenant_id <- 1..3 do
+      command = %RegisterTenant{tenant_id: tenant_id, name: "Tenant #{tenant_id}"}
+
+      assert :ok = TenantApp.dispatch(command)
+
+      name = TenantApp.tenant_application_name(tenant_id)
+      pid = Process.whereis(name)
+      assert is_pid(pid)
+    end
+  end
+
+  test "raise `RuntimeError` when application not started" do
+    command = %RegisterTenant{tenant_id: 4, name: "Tenant 4"}
+
+    assert_raise RuntimeError, fn ->
+      :ok = TenantApp.dispatch(command)
+    end
+  end
+end
diff --git a/test/middleware/support/tenant/tenant.ex b/test/middleware/support/tenant/tenant.ex
new file mode 100644
index 0000000..be7bc53
--- /dev/null
+++ b/test/middleware/support/tenant/tenant.ex
@@ -0,0 +1,33 @@
+defmodule Commanded.Middleware.Tenant do
+  defmodule Commands do
+    defmodule RegisterTenant do
+      @enforce_keys [:tenant_id, :name]
+      defstruct [:tenant_id, :name]
+    end
+  end
+
+  defmodule Events do
+    defmodule TenantRegistered do
+      @derive Jason.Encoder
+      defstruct [:tenant_id, :name]
+    end
+  end
+
+  alias Commanded.Middleware.Tenant
+  alias Commanded.Middleware.Tenant.Commands.RegisterTenant
+  alias Commanded.Middleware.Tenant.Events.TenantRegistered
+
+  defstruct [:tenant_id, :name]
+
+  def execute(%Tenant{tenant_id: nil}, %RegisterTenant{} = command) do
+    %RegisterTenant{tenant_id: tenant_id, name: name} = command
+
+    %TenantRegistered{tenant_id: tenant_id, name: name}
+  end
+
+  def apply(%Tenant{} = tenant, %TenantRegistered{} = event) do
+    %TenantRegistered{tenant_id: tenant_id, name: name} = event
+
+    %Tenant{tenant | tenant_id: tenant_id, name: name}
+  end
+end
diff --git a/test/middleware/support/tenant/tenant_app.ex b/test/middleware/support/tenant/tenant_app.ex
new file mode 100644
index 0000000..725c244
--- /dev/null
+++ b/test/middleware/support/tenant/tenant_app.ex
@@ -0,0 +1,11 @@
+defmodule Commanded.Middleware.TenantApp do
+  use Commanded.Application, otp_app: :commanded
+
+  alias Commanded.Middleware.TenantRouter
+
+  router(TenantRouter)
+
+  def tenant_application_name(tenant_id) do
+    Module.concat([__MODULE__, "tenant#{tenant_id}"])
+  end
+end
diff --git a/test/middleware/support/tenant/tenant_middleware.ex b/test/middleware/support/tenant/tenant_middleware.ex
new file mode 100644
index 0000000..c3ce695
--- /dev/null
+++ b/test/middleware/support/tenant/tenant_middleware.ex
@@ -0,0 +1,21 @@
+defmodule Commanded.Middleware.TenantMiddleware do
+  @behaviour Commanded.Middleware
+
+  alias Commanded.Middleware.Pipeline
+  alias Commanded.Middleware.TenantApp
+
+  def before_dispatch(%Pipeline{} = pipeline) do
+    %Pipeline{command: command} = pipeline
+
+    # Dynamically set application name from `tenant_id` in command
+    tenant_id = Map.fetch!(command, :tenant_id)
+    application = TenantApp.tenant_application_name(tenant_id)
+
+    %Pipeline{pipeline | application: application}
+  end
+
+  def before_dispatch(pipeline), do: pipeline
+
+  def after_dispatch(pipeline), do: pipeline
+  def after_failure(pipeline), do: pipeline
+end
diff --git a/test/middleware/support/tenant/tenant_router.ex b/test/middleware/support/tenant/tenant_router.ex
new file mode 100644
index 0000000..14c2010
--- /dev/null
+++ b/test/middleware/support/tenant/tenant_router.ex
@@ -0,0 +1,11 @@
+defmodule Commanded.Middleware.TenantRouter do
+  use Commanded.Commands.Router
+
+  alias Commanded.Middleware.Tenant
+  alias Commanded.Middleware.Tenant.Commands.RegisterTenant
+  alias Commanded.Middleware.TenantMiddleware
+
+  middleware(TenantMiddleware)
+
+  dispatch(RegisterTenant, to: Tenant, identity: :tenant_id)
+end