Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f01eba0
Add Mediator migration design spec
neozhu Mar 29, 2026
a4f5410
Add Mediator migration implementation plan
neozhu Mar 29, 2026
c57087e
chore: ignore local worktrees
neozhu Mar 29, 2026
afc9785
Add mediator compatibility safety-net tests
neozhu Mar 29, 2026
a81d8b2
Tighten mediator compatibility safety-net tests
neozhu Mar 29, 2026
345a905
Strengthen scoped mediator scope test
neozhu Mar 29, 2026
7e571c1
Add mediator compatibility safety-net tests
neozhu Mar 29, 2026
22c5e19
Tighten mediator compatibility safety-net tests
neozhu Mar 29, 2026
512a3e0
Strengthen scoped mediator scope test
neozhu Mar 29, 2026
b6ee351
docs: clarify mediator runtime test surface
neozhu Mar 29, 2026
6ce1a0a
docs: clarify mediator runtime test surface
neozhu Mar 29, 2026
6d332ac
Verify scoped mediator scope disposal
neozhu Mar 29, 2026
a2e060d
Verify scoped mediator scope disposal
neozhu Mar 29, 2026
52479d9
feat: add mediator compatibility contracts
neozhu Mar 29, 2026
17dc34f
fix: keep mediator compatibility contracts scoped
neozhu Mar 29, 2026
8d5ec39
fix: restore task 2 scope boundaries
neozhu Mar 29, 2026
59222e2
feat: add compile-only mediator registration shim
neozhu Mar 29, 2026
7fc1c1b
refactor: move mediator contracts to abstractions
neozhu Mar 29, 2026
ca06630
Wire Mediator source generator runtime
neozhu Mar 29, 2026
3df5c17
fix: deduplicate mediator pipeline registration
neozhu Mar 29, 2026
b6d1a6c
test: fix localized constant string assertions
neozhu Mar 29, 2026
0c4c134
Merge branch 'migrate-mediatr-to-mediator-main-impl' into migrate-med…
neozhu Mar 29, 2026
1a43a1e
chore: update package references and .gitignore for consistency
neozhu Mar 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Ignore Visual Studio temporary files, build results, and
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
Expand Down Expand Up @@ -358,4 +358,5 @@ MigrationBackup/
.vscode

**/Files/
.worktrees/
src/Server.UI/appsettings.development.json
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Experience the application in action:
| Layer | Technologies |
|-------|-------------|
| **Frontend** | Blazor Server, MudBlazor, SignalR |
| **Backend** | .NET 10, ASP.NET Core, MediatR, FluentValidation |
| **Backend** | .NET 10, ASP.NET Core, Mediator, FluentValidation |
| **Database** | Entity Framework Core, MSSQL/PostgreSQL/SQLite |
| **Authentication** | ASP.NET Core Identity, OAuth 2.0, JWT |
| **Caching** | FusionCache, Redis |
Expand Down Expand Up @@ -193,7 +193,7 @@ OpenSpec enables spec-driven, reviewable changes with clear proposals, deltas, a
- Use the patterns in `openspec/project.md`:
- For data access in handlers use `IApplicationDbContextFactory` and per-operation context lifetime:
- `await using var db = await _dbContextFactory.CreateAsync(cancellationToken);`
- Follow MediatR pipeline behaviors, caching tags, and specification patterns.
- Follow mediator pipeline behaviors, caching tags, and specification patterns.
- Mirror the Contacts module for a new entity's DTOs, commands, queries, specs, security, and UI pages/components.

5) Archive after deployment
Expand Down
173 changes: 173 additions & 0 deletions docs/superpowers/plans/2026-03-29-migrate-mediatr-to-mediator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Migrate MediatR To Mediator Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Execute one task at a time with spec review first, then code-quality review.

**Goal:** Remove `MediatR` references from the repository, move shared contracts to `Mediator.Abstractions`, and then reconnect runtime registration to `Mediator.SourceGenerator` in the outer layer.

**Architecture:** This plan no longer preserves a repo-owned `MediatR` facade. Shared projects should reference `Mediator.Abstractions` directly. Temporary compatibility files created during earlier migration attempts must be deleted. Runtime registration belongs in infrastructure or another outer executable-facing layer, not in the shared contract layer.

**Tech Stack:** .NET 10, Blazor Server, NUnit, FluentAssertions, `Mediator.Abstractions`, `Mediator.SourceGenerator`, Microsoft DI

---

## Task 1: Add Migration Safety-Net Tests

Status: completed on branch `migrate-mediatr-to-mediator-main-impl`.

Keep the existing safety-net tests. They intentionally prove that low-level `Mediator` contracts are not fully wired yet.

## Task 2: Replace MediatR Contracts With Direct Mediator.Abstractions Usage

**Intent:** Delete the temporary MediatR compatibility layer and move the shared contract surface to `Mediator.Abstractions` without yet completing the runtime registration rewrite.

**Files:**
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/Contracts.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/Handlers.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/PipelineContracts.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/PreProcessorContracts.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/ExceptionContracts.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/NotificationPublishing.cs`
- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/ServiceCollectionContracts.cs`
- Modify: `src/Domain/Domain.csproj`
- Modify: `src/Application/_Imports.cs`
- Modify: `src/Domain/Common/DomainEvent.cs`
- Modify: `tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs`

- [ ] **Step 1: Write the failing direct-abstractions smoke test**

Rewrite `tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs` into a direct `Mediator.Abstractions` smoke test. It should prove the project compiles against `Mediator.INotification`, `Mediator.IRequest<TResponse>`, and `Mediator.IPipelineBehavior<TRequest, TResponse>` rather than the deleted MediatR compatibility state type.

Suggested shape:

```csharp
using Mediator;
using NUnit.Framework;

namespace CleanArchitecture.Blazor.Application.UnitTests.Common.MediatorCompatibility;

public class MediatorAbstractionsSmokeTests
{
[Test]
public void Contracts_ShouldCompileAgainstMediatorAbstractions()
{
_ = new SmokeNotification();
_ = new SmokeRequest();
_ = typeof(IPipelineBehavior<SmokeRequest, string>);
}

private sealed record SmokeNotification : INotification;
private sealed record SmokeRequest : IRequest<string>;
}
```

- [ ] **Step 2: Run the smoke test to confirm it fails**

Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~MediatorAbstractionsSmokeTests" -v minimal`

Expected: FAIL because `Mediator.Abstractions` is not referenced yet.

- [ ] **Step 3: Add direct Mediator.Abstractions package support**

Update `src/Domain/Domain.csproj` to reference `Mediator.Abstractions`.

Keep the dependency in the lowest shared layer that exposes `DomainEvent` and shared mediator contracts.

- [ ] **Step 4: Remove the temporary MediatR compatibility files**

Delete all files under `src/Domain/Common/MediatorCompatibility/MediatR/`.

Do not replace them with new repo-owned MediatR shims.

- [ ] **Step 5: Update shared imports and domain event contracts**

Apply the smallest source edits needed so the shared code targets `Mediator` directly:

- `src/Application/_Imports.cs`: replace `global using MediatR;` and `global using MediatR.Pipeline;` with direct `Mediator` imports.
- `src/Domain/Common/DomainEvent.cs`: replace `global::MediatR` usage with `global::Mediator`.

- [ ] **Step 6: Add the temporary AddMediatR compile shim**

Because the runtime registration rewrite has not happened yet, keep `src/Application/DependencyInjection.cs` compiling by adding the smallest temporary `AddMediatR(...)` shim needed for this stage.

Constraints:

- The shim is transitional only.
- It must not recreate the deleted MediatR contract surface.
- It should exist only to let Task 2 land while Task 3 still owns the real DI rewrite.

- [ ] **Step 7: Re-run the smoke test**

Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~MediatorAbstractionsSmokeTests" -v minimal`

Expected: PASS, or at worst fail for a known unrelated baseline issue that already existed before this migration work.

- [ ] **Step 8: Run the scoped mediator safety-net test**

Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~ScopedMediatorTests" -v minimal`

Expected: The test should move closer to green by compiling against real `Mediator` abstractions. If it still fails, the remaining failure should be due to runtime wiring, not missing MediatR contracts.

- [ ] **Step 9: Commit**

```bash
git add src/Domain/Domain.csproj src/Application/_Imports.cs src/Domain/Common/DomainEvent.cs tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs src/Application/DependencyInjection.cs
git rm -r src/Domain/Common/MediatorCompatibility/MediatR
git commit -m "refactor: move mediator contracts to abstractions"
```

## Task 3: Replace DI Registration With Mediator.SourceGenerator Runtime Wiring

**Intent:** Remove the temporary `AddMediatR(...)` bridge, register the real `Mediator` runtime in the outer layer, and reconnect handlers, notifications, behaviors, and scoped mediator execution.

**Files:**
- Modify: `src/Application/DependencyInjection.cs`
- Modify: `src/Infrastructure/DependencyInjection.cs`
- Modify: `src/Infrastructure/Infrastructure.csproj`
- Modify: `src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs`
- Modify: `tests/Application.IntegrationTests/Testing.cs`
- Modify: `tests/Application.IntegrationTests/Common/MediatorCompatibility/MediatorCompatibilityTests.cs`
- Modify: `tests/Application.UnitTests/Common/MediatorCompatibility/ParallelNoWaitPublisherTests.cs`
- Modify: `tests/Application.UnitTests/Infrastructure/MediatorCompatibility/ScopedMediatorTests.cs`

Notes:

- Add `Mediator.SourceGenerator` in the outer layer only.
- Delete the temporary `AddMediatR(...)` shim as part of this task.
- Reconnect `ParallelNoWaitPublisher`, scoped mediator resolution, and at least one existing query path through DI.
- Decide explicitly how current exception-handler behavior maps to `Mediator`. Do not assume a drop-in equivalent exists.

## Task 4: Update UI/Test Imports And Resolve Remaining Runtime Gaps

**Files:**
- Modify: `src/Server.UI/_Imports.razor`
- Modify: `src/Server.UI/Services/DialogServiceHelper.cs`
- Modify: `tests/Application.IntegrationTests/Testing.cs`
- Modify: any source or test files still importing `MediatR`

Notes:

- Remove leftover `MediatR` namespace imports after runtime wiring is stable.
- Keep `IScopedMediator` intact as a repository abstraction if it still provides value.

## Task 5: Remove Leftover MediatR References And Update Documentation

**Files:**
- Modify: `README.md`
- Modify: `src/Server.UI/Pages/Public/Index.razor`
- Modify: `src/Server.UI/Pages/AI/Chatbot.razor`
- Modify: any `.csproj` files still referencing `MediatR`

Verification:

- `rg -n "MediatR" src tests README.md`
- Update user-facing copy so the app no longer claims it uses MediatR.

## Final Verification

Before claiming the migration complete, run:

- `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj -v minimal`
- `dotnet test tests/Application.IntegrationTests/Application.IntegrationTests.csproj --filter "FullyQualifiedName~MediatorCompatibilityTests" -v minimal`
- `rg -n "MediatR" src tests README.md`

If any command fails, report the exact remaining blocker instead of declaring success.
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Migrate MediatR To Mediator Design

## Status

Approved for planning and implementation.

## Summary

Migrate the repository from `MediatR` to `martinothamar/Mediator` by deleting `MediatR` package and namespace dependencies rather than preserving a repository-owned MediatR-compatible facade.

This migration is still conservative about behavior, but it is no longer conservative about API shape:

- Application, domain, infrastructure, UI, and tests should move to `Mediator` / `Mediator.Abstractions` namespaces directly.
- `Mediator.Abstractions` should be referenced by projects that only need contracts.
- `Mediator.SourceGenerator` should be referenced only by the outer runtime layer that owns DI wiring and generated handler dispatch.
- Existing behaviors such as request handling, notification publishing, domain event dispatch, and scoped mediator usage should remain intact or be reconnected with equivalent semantics.

## Goals

- Remove direct `MediatR` references from source, tests, and project files.
- Move shared contracts to `Mediator.Abstractions`.
- Preserve current application behavior for request sending, notification publishing, and scoped mediator usage.
- Prepare the repository for a later runtime registration step that adds `Mediator.SourceGenerator` in the correct outer layer.

## Non-Goals

- Do not keep or expand the temporary repo-owned `MediatR` compatibility layer.
- Do not redesign the command/query architecture.
- Do not change feature behavior unless required by the mediator migration.
- Do not pull `Mediator.SourceGenerator` into low-level shared projects that only need abstractions.

## Current State

The repository currently contains three conflicting mediator shapes:

- Legacy source files still import `MediatR`.
- Temporary compatibility contracts exist under `src/Domain/Common/MediatorCompatibility/MediatR/*`.
- Migration safety-net tests already target low-level `Mediator` runtime concepts in a few places.

That mixed state is unstable. The next implementation step must simplify the contract layer instead of adding more compatibility code.

## Approved Architecture

Use direct `Mediator` abstractions throughout the codebase.

### Contract Layer

- `Domain` references `Mediator.Abstractions`.
- `Application` imports `Mediator` namespaces directly for requests, notifications, handlers, behaviors, preprocessors, and exception handlers.
- `DomainEvent` implements `Mediator.INotification` directly.
- UI and integration tests may resolve `Mediator.IMediator` directly once runtime wiring is in place.

### Runtime Layer

- `Infrastructure` will eventually own `Mediator.SourceGenerator` and registration.
- `Application/DependencyInjection.cs` must stop depending on `AddMediatR(...)`.
- `IScopedMediator` remains as a repository abstraction, but its implementation should resolve the real `Mediator.IMediator` service from a created scope.

### Temporary Transition Rule

Until the runtime registration task lands:

- Contract-only work may add `Mediator.Abstractions`.
- Temporary `MediatR` compatibility files should be removed instead of expanded.
- A minimal compile shim for `AddMediatR(...)` is allowed only as a short-lived bridge while the old DI registration still exists, but it must not become part of the final architecture.

## Behavioral Requirements

The migration must preserve or re-establish these behaviors:

- Existing request handlers still execute for current commands and queries.
- Existing notification handlers still execute for current notifications and domain events.
- `ParallelNoWaitPublisher` remains available and keeps its fire-and-forget behavior if the runtime layer still needs that strategy.
- `DispatchDomainEventsInterceptor` continues to publish domain events through `IScopedMediator`.
- `ScopedMediator` continues to resolve a fresh scoped `IMediator`.

## File Organization

Expected migration touchpoints:

- `src/Domain/Domain.csproj`
- `src/Application/_Imports.cs`
- `src/Application/DependencyInjection.cs`
- `src/Domain/Common/DomainEvent.cs`
- `src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs`
- `src/Server.UI/_Imports.razor`
- `src/Server.UI/Services/DialogServiceHelper.cs`
- `tests/Application.IntegrationTests/Testing.cs`
- `tests/Application.UnitTests/Common/MediatorCompatibility/*`
- `tests/Application.IntegrationTests/Common/MediatorCompatibility/*`

Files under `src/Domain/Common/MediatorCompatibility/MediatR/*` are temporary and should be deleted rather than treated as a stable design boundary.

## Migration Sequence

1. Keep the safety-net tests from Task 1.
2. Remove the temporary MediatR compatibility contracts.
3. Add `Mediator.Abstractions` where compile-time contracts are required.
4. Update source imports and contract usage to direct `Mediator` namespaces.
5. Keep DI compiling with the smallest temporary bridge necessary until runtime registration is replaced.
6. In a later task, add the real `Mediator.SourceGenerator` runtime registration in the outer layer and reconnect behaviors.
7. Remove any temporary DI shims before completion.

## Verification Scope

At minimum, verify:

- Contract-layer projects compile against `Mediator.Abstractions`.
- Safety-net tests that intentionally exercise `Mediator` low-level contracts compile in the expected direction.
- `IScopedMediator` still resolves `IMediator` from a created scope after runtime wiring is updated.
- There are no leftover `MediatR` package references or stable source dependencies outside temporary transitional code that is explicitly scheduled for deletion.

## Risks

- `Mediator` does not provide a drop-in equivalent for every MediatR interface, so request exception handling and DI registration need explicit redesign instead of namespace swapping.
- The temporary compatibility files can hide architectural drift if they are left in place.
- `AddMediatR(...)` currently anchors application startup, so contract-layer migration and runtime registration need to be staged carefully.

## Success Criteria

The migration is complete when:

- The repository no longer depends on `MediatR`.
- Shared code compiles against `Mediator.Abstractions` instead of `MediatR`.
- Runtime registration is owned by `Mediator.SourceGenerator` in the correct outer layer.
- Scoped mediator, notifications, and domain events still work.
- Documentation accurately describes the repository as using `Mediator`, not `MediatR`.
9 changes: 5 additions & 4 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<LangVersion>default</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />

<PackageReference Include="Ardalis.Specification" Version="9.3.1" />
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="9.3.1" />
Expand All @@ -23,10 +24,10 @@
<PackageReference Include="Riok.Mapperly" Version="5.0.0-next.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.7.1" />
<PackageReference Include="Hangfire.Core" Version="1.8.23" />
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.5.0" />
<PackageReference Include="ActualLab.Fusion" Version="12.1.98" />
<PackageReference Include="ActualLab.Fusion.Blazor" Version="12.1.98" />
<PackageReference Include="ActualLab.Generators" Version="12.1.98">
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.6.0" />
<PackageReference Include="ActualLab.Fusion" Version="12.1.125" />
<PackageReference Include="ActualLab.Fusion.Blazor" Version="12.1.125" />
<PackageReference Include="ActualLab.Generators" Version="12.1.125">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
17 changes: 10 additions & 7 deletions src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

namespace CleanArchitecture.Blazor.Application.Common.ExceptionHandlers;

public class DbExceptionHandler<TRequest, TResponse, TException> : IRequestExceptionHandler<TRequest, TResponse, TException>
public class DbExceptionHandler<TRequest, TResponse> : MessageExceptionHandler<TRequest, TResponse>
where TRequest : IRequest<Result<int>>
where TResponse : Result<int>
where TException : DbUpdateException
{
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
Expand All @@ -14,14 +13,18 @@
{

_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger(nameof(DbExceptionHandler<TRequest, TResponse, TException>));
_logger = _loggerFactory.CreateLogger(nameof(DbExceptionHandler<TRequest, TResponse>));
}

public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state,
protected override ValueTask<ExceptionHandlingResult<TResponse>> Handle(TRequest request, Exception exception,

Check failure on line 19 in src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename parameter 'request' to 'message' to match the base class declaration.

See more on https://sonarcloud.io/project/issues?id=neozhu_CleanArchitectureWithBlazorServer&issues=AZ05YyXNPAYx8a-Quko4&open=AZ05YyXNPAYx8a-Quko4&pullRequest=928
CancellationToken cancellationToken)
{
state.SetHandled((TResponse)Result<int>.Failure(GetErrors(exception)));
return Task.CompletedTask;
if (exception is not DbUpdateException dbUpdateException)
{
return NotHandled;
}

return Handled((TResponse)Result<int>.Failure(GetErrors(dbUpdateException)));
}

private string[] GetErrors(DbUpdateException exception)
Expand Down Expand Up @@ -82,4 +85,4 @@
};
}

}
}
Loading
Loading