Migrating from V1 to V2
View SourceOverview
V2 introduces a cleaner middleware API that removes the need for EctoMiddleware.Super and gives middleware full control over execution flow. V1 middleware continue to work but emit deprecation warnings.
This guide will help you migrate your V1 middleware to V2. We'll use EctoHooks as a real-world example of V1 middleware.
What V1 Middleware Looks Like
Here's a V1 middleware from EctoHooks that runs before database operations:
defmodule EctoHooks do
defmacro __using__(_opts) do
quote do
# ... other code ...
def middleware(_action, _resource) do
[
EctoHooks.Middleware.Before,
EctoMiddleware.Super,
EctoHooks.Middleware.After
]
end
end
end
end
defmodule EctoHooks.Middleware.Before do
@behaviour EctoMiddleware
@impl EctoMiddleware
def middleware(resource, resolution) when resolution.action in [:insert, :insert!] do
EctoHooks.before_insert(resource, resolution.action)
end
def middleware(resource, resolution) when resolution.action in [:update, :update!] do
EctoHooks.before_update(resource, resolution.action)
end
def middleware(resource, _resolution) do
resource
end
endV1 middleware had limitations:
- Required
EctoMiddleware.Superto determine if a middleware is run before/after repo operation - Couldn't halt execution cleanly
- No control over middleware flow
- Couldn't easily wrap operations (before + after in same middleware)
- Couldn't override repo action entirely
Breaking Changes
1. Repo Setup
Before (V1):
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
use EctoMiddleware # Deprecated
endAfter (V2):
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
use EctoMiddleware.Repo
end2. yield/2 Return Value
If you're using a middleware intended to run before the repo operation, you need to make the following change:
Before (V1):
def middleware(resource, resolution) do
result = yield(resource, resolution)
transform(result)
endAfter (V2):
@impl EctoMiddleware
def process_before(resource, resolution) do
{result, _updated_resolution} = yield(resource, resolution)
{:cont, transform(result)}
endDitto for after middleware:
Before (V1):
def middleware(result, resolution) do
result = yield(result, resolution)
transform(result)
endAfter (V2):
@impl EctoMiddleware
def process_after(result, resolution) do
{result, _updated_resolution} = yield(result, resolution)
{:cont, transform(result)}
endIf your middleware is intended to ship with both before and after logic, you can now do that in a single middleware using process/2:
@impl EctoMiddleware
def process(resource, resolution) do
# Before logic
transformed = transform_before(resource)
# Yield to repo operation
{result, _updated_resolution} = yield(transformed, resolution)
# After logic
final_result = transform_after(result)
endMigration Strategies
For most V1 middleware, you can use a simple pattern. EctoMiddleware includes V1 wrapper implementations you can reference:
EctoMiddleware.V1.Before- Wraps V1 before middlewareEctoMiddleware.V1.After- Wraps V1 after middleware
These show a verbose but generalizable pattern that works for any V1 middleware.
Real-World Examples
Example 1: Simple Transformation
V1:
defmodule MyApp.Middleware.AddTimestamp do
@behaviour EctoMiddleware
def middleware(changeset, _resolution) do
Ecto.Changeset.put_change(changeset, :updated_at, DateTime.utc_now())
end
end
# In Repo:
def middleware(:update, _resource) do
[AddTimestamp, EctoMiddleware.Super]
endV2:
defmodule MyApp.Middleware.AddTimestamp do
use EctoMiddleware
@impl EctoMiddleware
def process_before(changeset, _resolution) do
{:cont, Ecto.Changeset.put_change(changeset, :updated_at, DateTime.utc_now())}
end
end
# In Repo:
@impl EctoMiddleware.Repo
def middleware(:update, _resource) do
[AddTimestamp] # No Super needed
endExample 2: Result Processing
V1:
defmodule MyApp.Middleware.EnrichUser do
@behaviour EctoMiddleware
def middleware(user, _resolution) do
%{user | full_name: "#{user.first_name} #{user.last_name}"}
end
end
# In Repo:
def middleware(:get, User) do
[EctoMiddleware.Super, EnrichUser]
endV2:
defmodule MyApp.Middleware.EnrichUser do
use EctoMiddleware
@impl EctoMiddleware
def process_after({:ok, user}, _resolution) do
{:cont, {:ok, %{user | full_name: "#{user.first_name} #{user.last_name}"}}}
end
def process_after(other, _resolution), do: {:cont, other}
end
# In Repo:
def middleware(:get, User) do
[EnrichUser] # No Super needed
endExample 3: Before + After (V2 Improvement)
V1 (Required Two Middleware):
defmodule MyApp.Middleware.LogBefore do
@behaviour EctoMiddleware
def middleware(resource, resolution) do
Logger.info("Before #{resolution.action}")
resource
end
end
defmodule MyApp.Middleware.LogAfter do
@behaviour EctoMiddleware
def middleware(result, resolution) do
Logger.info("After #{resolution.action}: #{inspect(result)}")
result
end
end
# In Repo:
def middleware(:insert, _) do
[LogBefore, EctoMiddleware.Super, LogAfter]
endV2 (Single Middleware):
defmodule MyApp.Middleware.Logger do
use EctoMiddleware
@impl EctoMiddleware
def process(resource, resolution) do
Logger.info("Before #{resolution.action}")
{result, _updated_resolution} = yield(resource, resolution)
Logger.info("After #{resolution.action}: #{inspect(result)}")
result
end
end
# In Repo:
def middleware(:insert, _) do
[Logger] # Single middleware!
endReturn Value Conventions
process_before/2 and process_after/2
Must return {:cont, value} to continue or {:halt, value} to stop:
def process_before(changeset, _resolution) do
if valid?(changeset) do
{:cont, transform(changeset)}
else
{:halt, {:error, :invalid}}
end
endFor convenience, you can return bare values, but this will emit deprecation warnings:
def process_before(changeset, _resolution) do
transform(changeset) # Works but warns
endprocess/2
Can return anything. The value you return is what gets passed up the chain:
def process(resource, resolution) do
{result, _} = yield(resource, resolution)
transform(result) # This is what the next middleware sees
endUse {:halt, value} to explicitly signal halting (optional but recommended for clarity):
def process(resource, resolution) do
if should_halt?(resource) do
{:halt, {:error, :stopped}} # Explicit
else
{result, _} = yield(resource, resolution)
result
end
endReference Implementations
For a complete, generalizable pattern that works for any V1 middleware, see the V1 wrapper implementations in EctoMiddleware:
EctoMiddleware.V1.Before- Shows how V1 before middleware is wrapped in V2EctoMiddleware.V1.After- Shows how V1 after middleware is wrapped in V2
These implementations are verbose but demonstrate the pattern you can follow to migrate any V1 middleware. However, many middleware can be simplified by using V2's process/2 callback instead of separate before/after middleware.
Removed Features
EctoMiddleware.Super
Not needed in V2. Just list your middleware without Super:
# V1
def middleware(:insert, _resource) do
[Before1, Before2, EctoMiddleware.Super, After1, After2]
end
# V2
def middleware(:insert, _resource) do
[Before1, Before2, After1, After2]
endResolution Fields (Deprecated)
The following resolution fields will be removed in v3.0:
before_inputbefore_outputafter_inputafter_output
Don't rely on these in new code. Use resolution.private to pass data between middleware instead.
Silencing Deprecation Warnings
During migration, you may want to silence warnings temporarily:
# In config/config.exs
config :ecto_middleware, :silence_deprecation_warnings, trueThis should only be used during migration - deprecated APIs will be removed in v3.0.
Testing Your Migration
- Update your Repo to use
EctoMiddleware.Repo - Run your test suite - V1 middleware should still work with warnings
- Migrate middleware one at a time, running tests after each
- Once all middleware are migrated, warnings should stop
Need Help?
See the API documentation for detailed examples and common patterns.