Skip to content

feat: Integrate MediatR and update benchmarks for improved command and notification handling#28

Merged
hard-rox merged 3 commits intomainfrom
mediator-like-interface
Apr 16, 2026
Merged

feat: Integrate MediatR and update benchmarks for improved command and notification handling#28
hard-rox merged 3 commits intomainfrom
mediator-like-interface

Conversation

@hard-rox
Copy link
Copy Markdown
Owner

@hard-rox hard-rox commented Apr 16, 2026

Summary

Describe the change.

Testing

  • dotnet test
  • Manual verification (describe)

Risks

List any risks, migrations, or breaking changes.

Summary by CodeRabbit

  • New Features

    • Added MediatR-compatible request/handler API and a unified Send request path; commands/queries interoperate with the dispatcher. Pipeline behaviors can be scoped to commands, queries, or all requests; Unit/void commands are handled compatibly.
  • Documentation

    • Expanded migration guide, README, getting-started, and advanced-usage with MediatR examples, pipeline scoping, Unit semantics, and updated usage snippets. Tests noted under tests/.
  • Benchmarks

    • Added MediatR vs ProDispatch benchmark comparisons.
  • Samples

    • Added command-only and query-only pipeline behavior examples and registrations.

…terfaces

- Updated migration documentation to reflect ProDispatch's compatibility with MediatR's IRequest/IRequestHandler and added CQRS-specific interfaces.
- Introduced CommandOnlyBehavior and QueryOnlyBehavior to demonstrate scoped pipeline behaviors for commands and queries.
- Refactored InProcessDispatcher to support both MediatR-style and CQRS-style requests, including enhanced behavior application based on request type.
- Added IRequest and IRequestHandler interfaces to support request handling without a result.
- Implemented ICommand and IQuery interfaces to extend MediatR's functionality for clearer domain modeling.
- Introduced ICommandPipelineBehavior and IQueryPipelineBehavior marker interfaces for behavior scoping.
- Updated DI registration examples in console and minimal API samples to include new behaviors and request handling.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d134ab70-7d8b-451c-83cb-24d4c72488d5

📥 Commits

Reviewing files that changed from the base of the PR and between ca539fd and 0c4b940.

📒 Files selected for processing (1)
  • Directory.Packages.props
🚧 Files skipped from review as they are similar to previous changes (1)
  • Directory.Packages.props

📝 Walkthrough

Walkthrough

ProDispatch now supports MediatR-style IRequest<TResponse>/IRequestHandler<TRequest,TResponse> alongside CQRS ICommand/IQuery types, adds marker-based pipeline scoping (ICommandPipelineBehavior, IQueryPipelineBehavior), and updates InProcessDispatcher to prefer IRequestHandler resolution with fallback to command/query handlers and unified pipeline construction.

Changes

Cohort / File(s) Summary
Request/Handler Abstractions
src/ProDispatch/Abstractions/Requests/IRequest.cs, src/ProDispatch/Abstractions/Requests/IRequestHandler.cs
Add MediatR-compatible IRequest<out TResponse> and IRequestHandler<in TRequest, TResponse> (and non-generic variants).
Command/Query Interface Updates
src/ProDispatch/Abstractions/Commands/ICommand.cs, src/ProDispatch/Abstractions/Commands/ICommandHandler.cs, src/ProDispatch/Abstractions/Queries/IQuery.cs, src/ProDispatch/Abstractions/Queries/IQueryHandler.cs
Make ICommand/IQuery inherit IRequest variants; update handlers to implement IRequestHandler<,> with explicit interface forwards and new on existing HandleAsync signatures.
Pipeline Scoping
src/ProDispatch/Abstractions/Pipeline/ICommandPipelineBehavior.cs, src/ProDispatch/Abstractions/Pipeline/IQueryPipelineBehavior.cs, src/ProDispatch/Abstractions/Pipeline/PipelineScope.cs
Add marker interfaces and PipelineScope enum to classify behaviors as all/commands-only/queries-only.
Dispatcher Abstractions
src/ProDispatch/Abstractions/Dispatcher/IDispatcher.cs, src/ProDispatch/Abstractions/Dispatcher/IMediator.cs
Add Send<TResponse>(IRequest<TResponse>, ...) to IDispatcher; introduce obsolete IMediator as a MediatR-compatible bridge.
Core Dispatcher Implementation
src/ProDispatch/Dispatcher/InProcessDispatcher.cs
Introduce unified Send<TResponse> path, resolve IRequestHandler<,> first with fallback to ICommandHandler/IQueryHandler, build pipelines filtered by scope (incl. object-typed behaviors for Unit), and normalize handler return shapes (Task vs Task).
Global Usings
src/ProDispatch/GlobalUsings.cs
Added global using for ProDispatch.Abstractions.Requests.
Example Pipeline Behaviors
samples/.../Behaviors/CommandOnlyBehavior.cs, samples/.../Behaviors/QueryOnlyBehavior.cs
Add example scoped behaviors implementing ICommandPipelineBehavior / IQueryPipelineBehavior with console logging.
Example Program Registrations
samples/ProDispatch.Examples.Console/Program.cs, samples/ProDispatch.Examples.MinimalApi/Program.cs
Register new command-only and query-only behaviors alongside existing shared behaviors; adjust registration order.
Benchmark Setup
benchmarks/ProDispatch.Benchmarks/DispatcherBenchmarks.cs, benchmarks/ProDispatch.Benchmarks/ProDispatch.Benchmarks.csproj
Add MediatR benchmark suite and types; rename ProDispatch benchmarks with ProDispatch_ prefix; add MediatR & DI package references.
Configuration
Directory.Packages.props
Add centralized package versions for MediatR (12.4.1) and Microsoft.Extensions.DependencyInjection (10.0.1).
Documentation
.github/copilot-instructions.md, README.md, docs/getting-started.md, docs/advanced-usage.md, docs/migration-from-mediatr.md
Extensive docs updates: MediatR compatibility, examples, pipeline scoping semantics, Unit handling, and migration guidance.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Dispatcher as InProcessDispatcher
    participant Resolver as HandlerResolver
    participant PipelineBuilder as Pipeline Builder
    participant Behavior as Pipeline Behaviors
    participant Handler as Final Handler

    Client->>Dispatcher: Send<TResponse>(IRequest<TResponse>)
    activate Dispatcher
    Dispatcher->>Resolver: Resolve IRequestHandler<TRequest,TResponse>
    activate Resolver
    alt IRequestHandler found
        Resolver-->>Dispatcher: IRequestHandler instance
    else not found
        Resolver->>Resolver: Try ICommandHandler / IQueryHandler
        Resolver-->>Dispatcher: Fallback handler (if any)
    end
    deactivate Resolver

    Dispatcher->>PipelineBuilder: Collect IPipelineBehavior<TRequest,TResponse>
    activate PipelineBuilder
    PipelineBuilder->>PipelineBuilder: Include object-typed behaviors if TResponse==Unit
    PipelineBuilder->>PipelineBuilder: Filter by ICommandPipelineBehavior / IQueryPipelineBehavior markers
    PipelineBuilder-->>Dispatcher: Composed delegate chain
    deactivate PipelineBuilder

    Dispatcher->>Behavior: Execute outermost behavior
    activate Behavior
    loop each behavior -> next
        Behavior->>Behavior: HandleAsync(request, ct, next)
    end
    Behavior->>Handler: Invoke final handler
    activate Handler
    Handler-->>Behavior: Task<TResponse> or Task
    deactivate Handler
    Behavior-->>Dispatcher: Return TResponse
    deactivate Behavior
    Dispatcher-->>Client: Task<TResponse>
    deactivate Dispatcher
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Initial setup #1: Foundational PR that introduced the initial request/handler/pipeline abstractions and InProcessDispatcher implementation this change extends for MediatR compatibility and pipeline scoping.

Poem

🐰 I hopped through code with eager paws,

IRequest and ICommand join laws,
Behaviors scoped to query or quest,
Pipelines wrap and handlers test,
A carrot of compatibility — dispatch at rest.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR: integrating MediatR-style request handling and updating benchmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mediator-like-interface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
src/ProDispatch/Dispatcher/InProcessDispatcher.cs (2)

35-43: Consider adding TryGetInstance to avoid exception-based control flow.

Using exceptions for expected control flow (when a handler isn't registered) has performance overhead and can clutter debugger output. While acceptable for per-request dispatch, a cleaner approach would be:

♻️ Suggested IServiceFactory extension

Add to IServiceFactory:

object? TryGetInstance(Type serviceType);

Then simplify resolution:

-object? handler = null;
-try
-{
-    handler = _serviceFactory.GetInstance(handlerType);
-}
-catch (InvalidOperationException)
-{
-    // Fallback: Try to resolve specific command/query handler types
-    handler = TryResolveSpecificHandler(request, requestType, typeof(TResponse));
-}
+object? handler = _serviceFactory.TryGetInstance(handlerType)
+    ?? TryResolveSpecificHandler(request, requestType, typeof(TResponse));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProDispatch/Dispatcher/InProcessDispatcher.cs` around lines 35 - 43,
Replace the exception-driven resolution in InProcessDispatcher by adding a
TryGetInstance method to IServiceFactory and using it instead of GetInstance:
call _serviceFactory.TryGetInstance(handlerType) and check for null before
falling back to TryResolveSpecificHandler(request, requestType,
typeof(TResponse)); if TryGetInstance returns non-null assign it to handler,
otherwise call TryResolveSpecificHandler as the existing fallback. Update any
implementations of IServiceFactory to implement TryGetInstance to avoid throwing
InvalidOperationException for missing registrations and remove the try/catch
around _serviceFactory.GetInstance in the Dispatcher.

153-153: Unused responseType parameter - typeof(TResponse) is already available.

The responseType parameter is always passed as typeof(TResponse) from the caller. Since TResponse is a generic type parameter, you can use typeof(TResponse) directly within the method, eliminating the redundant parameter.

♻️ Suggested simplification
-private object? TryResolveSpecificHandler<TResponse>(IRequest<TResponse> request, Type requestType, Type responseType)
+private object? TryResolveSpecificHandler<TResponse>(IRequest<TResponse> request, Type requestType)
 {
     // ...
-    Type commandHandlerType = typeof(ICommandHandler<,>).MakeGenericType(requestType, responseType);
+    Type commandHandlerType = typeof(ICommandHandler<,>).MakeGenericType(requestType, typeof(TResponse));
     // ...
 }

Update call site at line 42:

-handler = TryResolveSpecificHandler(request, requestType, typeof(TResponse));
+handler = TryResolveSpecificHandler(request, requestType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProDispatch/Dispatcher/InProcessDispatcher.cs` at line 153, The method
TryResolveSpecificHandler<TResponse>(IRequest<TResponse> request, Type
requestType, Type responseType) has an unused responseType parameter since
typeof(TResponse) is available; change the signature to remove the responseType
parameter and use typeof(TResponse) inside TryResolveSpecificHandler, then
update all call sites that pass typeof(TResponse) (e.g., the call that currently
supplies responseType) to call the new two-parameter overload with just request
and requestType; ensure any reflection/type comparisons inside the method
reference typeof(TResponse) instead of the removed parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/ProDispatch.Benchmarks/DispatcherBenchmarks.cs`:
- Around line 80-89: The benchmark currently registers pipeline behaviors as
IPipelineBehavior<BenchmarkCommand, object>, forcing ProDispatch through the
object→Unit adapter and adding boxing/casting overhead; update the registrations
in BuildCommandDispatcherMultiplePipeline (and the similar registrations at
lines 111-114) to use the actual Unit response type instead (i.e.
IPipelineBehavior<BenchmarkCommand, Unit> / register NoOpBehavior for Unit) so
the “one pipeline” and “multiple pipeline” benchmarks measure the typed Unit
path, or alternatively add a separate benchmark method explicitly named for the
object-compatibility case to keep both comparisons.

In `@docs/advanced-usage.md`:
- Around line 4-6: The docs inversion: update the text explaining
registration/wrapping so it states that behaviors are returned by the service
factory in registration order but the dispatcher wraps them in reverse,
therefore the last-registered behavior runs closest to the handler (innermost)
and the first-registered runs outermost; change the example order to reflect
that logging/metrics should be registered first (outermost) and
validation/caching registered later (closer to handler), and correct the wording
around "Outer behaviors are registered last" to say "Outer behaviors are
registered first" or equivalent; reference the dispatcher and service factory
behavior in the same paragraph for clarity.

In `@docs/getting-started.md`:
- Around line 104-112: In HandleAsync (the generic middleware method handling
TRequest/TResponse), don't return directly from await next(...); instead call
and await next(request, cancellationToken) into a local variable (e.g.,
response), perform the cache write using that response, then return the
response; update references to next, request and cancellationToken accordingly
so the cache store occurs before the method returns.

In `@docs/migration-from-mediatr.md`:
- Around line 195-202: The snippet uses invalid open-generic object creation
(new ValidationBehavior<,>() / new LoggingBehavior<,>()), so update the example
to be compilable by either replacing those with concrete generic type arguments
(e.g., ValidationBehavior<MyRequest, MyResponse> and LoggingBehavior<MyRequest,
MyResponse>) in the factory.Register calls and show corresponding concrete
request/response types, or explicitly mark the block as pseudocode and provide a
separate concrete example using the concrete types; reference the
IPipelineBehavior<,>, ValidationBehavior, LoggingBehavior, and factory.Register
symbols so readers can locate and correct the code.

In `@README.md`:
- Around line 49-55: The handler examples in the README are non-compiling
because GetUserQueryHandler lacks a return path in its HandleAsync method and
other CQRS handler snippets implement interfaces without method bodies; update
GetUserQueryHandler.HandleAsync to include a minimal Task<User> return (e.g.,
return Task.FromResult<User>(null) or a placeholder User) and add minimal stub
implementations for the other handler methods (provide required
Handle/HandleAsync method bodies that return Task/Task<T> with placeholder
values) so the snippets compile as-is, or alternatively mark each snippet as
abbreviated pseudocode if you prefer not to include stubs.

In `@src/ProDispatch/Abstractions/Dispatcher/IMediator.cs`:
- Around line 11-13: IMediator currently only extends IDispatcher but lacks
adapter methods to preserve MediatR compatibility; add two bridge methods to the
IMediator interface — Task Publish(object notification, CancellationToken
cancellationToken = default) and Task Publish<TNotification>(TNotification
notification, CancellationToken cancellationToken = default) where TNotification
: INotification — and then implement them on the Dispatcher class to forward
calls to the existing IDispatcher.PublishAsync(...) methods (i.e., have
Dispatcher.Publish(...) call its PublishAsync(...) equivalents, passing through
the notification and cancellation token).

In `@src/ProDispatch/Abstractions/Pipeline/PipelineScope.cs`:
- Around line 1-22: The PipelineScope enum is unused and duplicates behavior
scoping currently expressed via marker interfaces ICommandPipelineBehavior and
IQueryPipelineBehavior; remove the dead enum PipelineScope from the codebase, or
if you prefer to standardize on the enum, refactor the pipeline
registration/dispatch logic to replace marker-interface checks with checks
against PipelineScope (update the dispatcher/pipeline registration code that
inspects ICommandPipelineBehavior/IQueryPipelineBehavior to instead read and
honor a PipelineScope property on pipeline behavior implementations). Ensure all
references are updated or removed so there are no unused symbols left
(PipelineScope, ICommandPipelineBehavior, IQueryPipelineBehavior, and any
dispatch logic relying on them).

In `@src/ProDispatch/Abstractions/Requests/IRequestHandler.cs`:
- Around line 8-10: IRequestHandler<TRequest> currently inherits HandleAsync
returning Task<UnitType>, so implementers must return UnitType; change the
contract to expose a no-result Task by adding a new Task HandleAsync(TRequest
request, CancellationToken cancellationToken) declaration (use the new modifier)
on IRequestHandler<TRequest>, and provide a default bridge implementation of
IRequestHandler<TRequest, UnitType>.HandleAsync that calls the new HandleAsync
and returns UnitType (e.g. async Task<UnitType> IRequestHandler<TRequest,
UnitType>.HandleAsync(...) { await HandleAsync(...); return UnitType.Value; })
so reflection-based resolution still finds HandleAsync; reference
IRequestHandler<TRequest>, IRequestHandler<TRequest, UnitType>,
IRequest<UnitType>, UnitType, HandleAsync and mirror the pattern used by
ICommandHandler<TCommand>.

---

Nitpick comments:
In `@src/ProDispatch/Dispatcher/InProcessDispatcher.cs`:
- Around line 35-43: Replace the exception-driven resolution in
InProcessDispatcher by adding a TryGetInstance method to IServiceFactory and
using it instead of GetInstance: call
_serviceFactory.TryGetInstance(handlerType) and check for null before falling
back to TryResolveSpecificHandler(request, requestType, typeof(TResponse)); if
TryGetInstance returns non-null assign it to handler, otherwise call
TryResolveSpecificHandler as the existing fallback. Update any implementations
of IServiceFactory to implement TryGetInstance to avoid throwing
InvalidOperationException for missing registrations and remove the try/catch
around _serviceFactory.GetInstance in the Dispatcher.
- Line 153: The method TryResolveSpecificHandler<TResponse>(IRequest<TResponse>
request, Type requestType, Type responseType) has an unused responseType
parameter since typeof(TResponse) is available; change the signature to remove
the responseType parameter and use typeof(TResponse) inside
TryResolveSpecificHandler, then update all call sites that pass
typeof(TResponse) (e.g., the call that currently supplies responseType) to call
the new two-parameter overload with just request and requestType; ensure any
reflection/type comparisons inside the method reference typeof(TResponse)
instead of the removed parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46de062a-bb36-405b-9523-ecbd31d0fdf7

📥 Commits

Reviewing files that changed from the base of the PR and between e3cec2e and ca539fd.

📒 Files selected for processing (27)
  • .github/copilot-instructions.md
  • Directory.Packages.props
  • README.md
  • benchmarks/ProDispatch.Benchmarks/DispatcherBenchmarks.cs
  • benchmarks/ProDispatch.Benchmarks/ProDispatch.Benchmarks.csproj
  • docs/advanced-usage.md
  • docs/getting-started.md
  • docs/migration-from-mediatr.md
  • samples/ProDispatch.Examples.Console/Behaviors/CommandOnlyBehavior.cs
  • samples/ProDispatch.Examples.Console/Behaviors/QueryOnlyBehavior.cs
  • samples/ProDispatch.Examples.Console/Program.cs
  • samples/ProDispatch.Examples.MinimalApi/Behaviors/CommandOnlyBehavior.cs
  • samples/ProDispatch.Examples.MinimalApi/Behaviors/QueryOnlyBehavior.cs
  • samples/ProDispatch.Examples.MinimalApi/Program.cs
  • src/ProDispatch/Abstractions/Commands/ICommand.cs
  • src/ProDispatch/Abstractions/Commands/ICommandHandler.cs
  • src/ProDispatch/Abstractions/Dispatcher/IDispatcher.cs
  • src/ProDispatch/Abstractions/Dispatcher/IMediator.cs
  • src/ProDispatch/Abstractions/Pipeline/ICommandPipelineBehavior.cs
  • src/ProDispatch/Abstractions/Pipeline/IQueryPipelineBehavior.cs
  • src/ProDispatch/Abstractions/Pipeline/PipelineScope.cs
  • src/ProDispatch/Abstractions/Queries/IQuery.cs
  • src/ProDispatch/Abstractions/Queries/IQueryHandler.cs
  • src/ProDispatch/Abstractions/Requests/IRequest.cs
  • src/ProDispatch/Abstractions/Requests/IRequestHandler.cs
  • src/ProDispatch/Dispatcher/InProcessDispatcher.cs
  • src/ProDispatch/GlobalUsings.cs

Comment on lines +80 to +89
factory.Register(typeof(ProDispatch.Abstractions.Pipeline.IPipelineBehavior<BenchmarkCommand, object>), () => new NoOpBehavior());
return new(factory);
}

private static InProcessDispatcher BuildCommandDispatcherMultiplePipeline()
{
SimpleServiceFactory factory = new();
factory.Register<ICommandHandler<BenchmarkCommand>>(() => new BenchmarkCommandHandler());
factory.Register(typeof(IPipelineBehavior<BenchmarkCommand, object>), () => new NoOpBehavior());
factory.Register(typeof(IPipelineBehavior<BenchmarkCommand, object>), () => new NoOpBehavior());
factory.Register(typeof(ProDispatch.Abstractions.Pipeline.IPipelineBehavior<BenchmarkCommand, object>), () => new NoOpBehavior());
factory.Register(typeof(ProDispatch.Abstractions.Pipeline.IPipelineBehavior<BenchmarkCommand, object>), () => new NoOpBehavior());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Benchmark the typed Unit path separately from the compatibility path.

These registrations force ProDispatch through the dispatcher’s objectUnit adapter path, so the “one pipeline” and “multiple pipeline” numbers include extra boxing/casting overhead that the MediatR cases do not. If this is meant to be a head-to-head pipeline benchmark, switch NoOpBehavior and these registrations to the actual Unit response type, or add a second benchmark explicitly named as the object-compatibility case.

Also applies to: 111-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/ProDispatch.Benchmarks/DispatcherBenchmarks.cs` around lines 80 -
89, The benchmark currently registers pipeline behaviors as
IPipelineBehavior<BenchmarkCommand, object>, forcing ProDispatch through the
object→Unit adapter and adding boxing/casting overhead; update the registrations
in BuildCommandDispatcherMultiplePipeline (and the similar registrations at
lines 111-114) to use the actual Unit response type instead (i.e.
IPipelineBehavior<BenchmarkCommand, Unit> / register NoOpBehavior for Unit) so
the “one pipeline” and “multiple pipeline” benchmarks measure the typed Unit
path, or alternatively add a separate benchmark method explicitly named for the
object-compatibility case to keep both comparisons.

Comment thread docs/advanced-usage.md
Comment on lines 4 to +6
- Register behaviors per request type; the dispatcher wraps them in reverse registration order.
- Outer behaviors are registered first; innermost last.
- Typical order: logging/metrics -> caching -> validation -> handler.
- **Note:** Example behaviors (LoggingBehavior, ValidationBehavior) are provided in the sample projects under `Behaviors/` directory. They demonstrate common patterns but are not part of the core library.
- Outer behaviors are registered **last**; innermost registered **first**.
- Typical order: handler -> validation -> caching -> logging/metrics (inner to outer).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The registration-order guidance is inverted.

Line 4 is correct, but Line 5 flips the consequence: because wrapping happens in reverse, the last registered behavior runs closest to the handler, not outermost. With the order on Line 6, logging/metrics should be registered first and validation last.

Suggested doc fix
-- Outer behaviors are registered **last**; innermost registered **first**.
+- Outer behaviors are registered **first**; innermost registered **last**.

As per coding guidelines: Service factory returns behaviors in registration order, but dispatcher wraps them in reverse so last-registered behavior runs closest to the handler. Based on learnings: outermost behavior should usually be logging/metrics, innermost should be validation or business rules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/advanced-usage.md` around lines 4 - 6, The docs inversion: update the
text explaining registration/wrapping so it states that behaviors are returned
by the service factory in registration order but the dispatcher wraps them in
reverse, therefore the last-registered behavior runs closest to the handler
(innermost) and the first-registered runs outermost; change the example order to
reflect that logging/metrics should be registered first (outermost) and
validation/caching registered later (closer to handler), and correct the wording
around "Outer behaviors are registered last" to say "Outer behaviors are
registered first" or equivalent; reference the dispatcher and service factory
behavior in the same paragraph for clarity.

Comment thread docs/getting-started.md
Comment on lines +104 to +112
public async Task<TResponse> HandleAsync(
TRequest request,
CancellationToken cancellationToken,
Func<TRequest, CancellationToken, Task<TResponse>> next)
{
// Check cache
return await next(request, cancellationToken);
// Store in cache
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move the cache write before the return.

The // Store in cache step is after return await next(...), so this example never actually caches the response. Capture the result first, store it, then return it.

Suggested doc fix
     public async Task<TResponse> HandleAsync(
         TRequest request, 
         CancellationToken cancellationToken, 
         Func<TRequest, CancellationToken, Task<TResponse>> next)
     {
         // Check cache
-        return await next(request, cancellationToken);
-        // Store in cache
+        var response = await next(request, cancellationToken);
+        // Store in cache
+        return response;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async Task<TResponse> HandleAsync(
TRequest request,
CancellationToken cancellationToken,
Func<TRequest, CancellationToken, Task<TResponse>> next)
{
// Check cache
return await next(request, cancellationToken);
// Store in cache
}
public async Task<TResponse> HandleAsync(
TRequest request,
CancellationToken cancellationToken,
Func<TRequest, CancellationToken, Task<TResponse>> next)
{
// Check cache
var response = await next(request, cancellationToken);
// Store in cache
return response;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/getting-started.md` around lines 104 - 112, In HandleAsync (the generic
middleware method handling TRequest/TResponse), don't return directly from await
next(...); instead call and await next(request, cancellationToken) into a local
variable (e.g., response), perform the cache write using that response, then
return the response; update references to next, request and cancellationToken
accordingly so the cache store occurs before the method returns.

Comment on lines +195 to +202
```csharp
// MediatR registration order
services.AddTransient<IPipelineBehavior<,>, LoggingBehavior<,>>();
services.AddTransient<IPipelineBehavior<,>, ValidationBehavior<,>>();

// ProDispatch equivalent (register in reverse)
factory.Register(typeof(IPipelineBehavior<,>), () => new ValidationBehavior<,>());
factory.Register(typeof(IPipelineBehavior<,>), () => new LoggingBehavior<,>());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace the open-generic new expressions with a compilable example.

new ValidationBehavior<,>() and new LoggingBehavior<,>() are not valid C#, so this migration snippet cannot be copied or run as written. Use a concrete request/response pair here, or call out explicitly that the block is pseudocode.

Suggested doc fix
- factory.Register(typeof(IPipelineBehavior<,>), () => new ValidationBehavior<,>());
- factory.Register(typeof(IPipelineBehavior<,>), () => new LoggingBehavior<,>());
+ factory.Register(
+     typeof(IPipelineBehavior<CreateUserCommand, object>),
+     () => new ValidationBehavior<CreateUserCommand, object>());
+ factory.Register(
+     typeof(IPipelineBehavior<CreateUserCommand, object>),
+     () => new LoggingBehavior<CreateUserCommand, object>());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/migration-from-mediatr.md` around lines 195 - 202, The snippet uses
invalid open-generic object creation (new ValidationBehavior<,>() / new
LoggingBehavior<,>()), so update the example to be compilable by either
replacing those with concrete generic type arguments (e.g.,
ValidationBehavior<MyRequest, MyResponse> and LoggingBehavior<MyRequest,
MyResponse>) in the factory.Register calls and show corresponding concrete
request/response types, or explicitly mark the block as pseudocode and provide a
separate concrete example using the concrete types; reference the
IPipelineBehavior<,>, ValidationBehavior, LoggingBehavior, and factory.Register
symbols so readers can locate and correct the code.

Comment thread README.md
Comment on lines +49 to +55
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, User>
{
public Task<User> HandleAsync(GetUserQuery request, CancellationToken cancellationToken)
{
// ... fetch user
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the handler examples compile as written.

GetUserQueryHandler is missing a return path, and the CQRS handlers on Lines 65 and 69 implement interfaces with empty bodies. Anyone copying these snippets will hit compiler errors immediately. Add minimal HandleAsync stubs, or label the snippet as abbreviated pseudocode.

Also applies to: 65-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 49 - 55, The handler examples in the README are
non-compiling because GetUserQueryHandler lacks a return path in its HandleAsync
method and other CQRS handler snippets implement interfaces without method
bodies; update GetUserQueryHandler.HandleAsync to include a minimal Task<User>
return (e.g., return Task.FromResult<User>(null) or a placeholder User) and add
minimal stub implementations for the other handler methods (provide required
Handle/HandleAsync method bodies that return Task/Task<T> with placeholder
values) so the snippets compile as-is, or alternatively mark each snippet as
abbreviated pseudocode if you prefer not to include stubs.

Comment on lines +11 to +13
[Obsolete("Use IDispatcher instead. IMediator will be removed in a future version.", false)]
[SuppressMessage("Major Code Smell", "S1133:Deprecated code should be removed", Justification = "Obsolete attribute intentionally used for migration path")]
public interface IMediator : IDispatcher
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What methods does the current MediatR IMediatorinterface expose forSendandPublish?

💡 Result:

The current MediatR IMediator interface inherits from ISender and IPublisher, exposing the following methods for Send and Publish: Send methods (from ISender): - Task Send(IRequest request, CancellationToken cancellationToken = default) - Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest - Task<object?> Send(object request, CancellationToken cancellationToken = default) - IAsyncEnumerable CreateStream(IStreamRequest request, CancellationToken cancellationToken = default) - IAsyncEnumerable<object?> CreateStream(object request, CancellationToken cancellationToken = default) Publish methods (from IPublisher): - Task Publish(object notification, CancellationToken cancellationToken = default) - Task Publish(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification These are the exact method signatures from the official source code in the latest version (v14.1.0 as of 2026-03-03).

Citations:


🏁 Script executed:

# Find and examine the IDispatcher interface definition
fd -t f "IDispatcher.cs" --exec cat -n {}

Repository: hard-rox/prodispatch

Length of output: 2756


🏁 Script executed:

# Check IMediator usage patterns in the codebase
rg -t cs "IMediator" --no-heading -B 2 -A 2

Repository: hard-rox/prodispatch

Length of output: 5338


🏁 Script executed:

# Look for any test files or adapters related to IMediator
fd -t f -i "mediatr|mediator" --exec basename {} \;

Repository: hard-rox/prodispatch

Length of output: 102


🏁 Script executed:

# Check the migration guide to understand the expected migration path
cat -n migration-from-mediatr.md | head -100

Repository: hard-rox/prodispatch

Length of output: 121


🏁 Script executed:

# Find the migration guide file with full path
fd -t f "migration-from-mediatr.md" -x cat -n {}

Repository: hard-rox/prodispatch

Length of output: 11724


IMediator shim lacks adapter methods to bridge MediatR API differences.

The migration guide claims IMediator enables backward compatibility during transition (line 79–108), but the actual interface is empty with no adapter methods. Existing MediatR call sites using Publish() will fail to compile since IDispatcher only exposes PublishAsync(). The guide itself contradicts its claim at lines 119–120, showing method names actually differ: MediatR's Publish() vs. ProDispatch's PublishAsync().

To support gradual migration, add bridge methods to IMediator:

public interface IMediator : IDispatcher
{
    Task Publish(object notification, CancellationToken cancellationToken = default);
    Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
        where TNotification : INotification;
}

Then implement these on Dispatcher to forward to PublishAsync().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProDispatch/Abstractions/Dispatcher/IMediator.cs` around lines 11 - 13,
IMediator currently only extends IDispatcher but lacks adapter methods to
preserve MediatR compatibility; add two bridge methods to the IMediator
interface — Task Publish(object notification, CancellationToken
cancellationToken = default) and Task Publish<TNotification>(TNotification
notification, CancellationToken cancellationToken = default) where TNotification
: INotification — and then implement them on the Dispatcher class to forward
calls to the existing IDispatcher.PublishAsync(...) methods (i.e., have
Dispatcher.Publish(...) call its PublishAsync(...) equivalents, passing through
the notification and cancellation token).

Comment on lines +1 to +22
namespace ProDispatch.Abstractions.Pipeline;

/// <summary>
/// Defines the scope where a pipeline behavior should be applied.
/// </summary>
public enum PipelineScope
{
/// <summary>
/// Behavior applies to all requests (commands and queries).
/// </summary>
All,

/// <summary>
/// Behavior applies only to commands.
/// </summary>
CommandsOnly,

/// <summary>
/// Behavior applies only to queries.
/// </summary>
QueriesOnly
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of PipelineScope enum
echo "=== Searching for PipelineScope usage ==="
rg -n "PipelineScope" --type=cs

Repository: hard-rox/prodispatch

Length of output: 188


Remove the unused PipelineScope enum or integrate it into the pipeline behavior system.

The enum is not referenced anywhere in the codebase. The current dispatcher implementation uses marker interfaces (ICommandPipelineBehavior, IQueryPipelineBehavior) to determine behavior scope, making this enum dead code. Either remove it or refactor the implementation to use this enum instead of marker interfaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProDispatch/Abstractions/Pipeline/PipelineScope.cs` around lines 1 - 22,
The PipelineScope enum is unused and duplicates behavior scoping currently
expressed via marker interfaces ICommandPipelineBehavior and
IQueryPipelineBehavior; remove the dead enum PipelineScope from the codebase, or
if you prefer to standardize on the enum, refactor the pipeline
registration/dispatch logic to replace marker-interface checks with checks
against PipelineScope (update the dispatcher/pipeline registration code that
inspects ICommandPipelineBehavior/IQueryPipelineBehavior to instead read and
honor a PipelineScope property on pipeline behavior implementations). Ensure all
references are updated or removed so there are no unused symbols left
(PipelineScope, ICommandPipelineBehavior, IQueryPipelineBehavior, and any
dispatch logic relying on them).

Comment on lines +8 to +10
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, UnitType> where TRequest : IRequest<UnitType>
{
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Expose the no-result handler contract as Task, not Task<UnitType>.

IRequestHandler<TRequest> currently adds no member of its own, so implementers still have to satisfy the inherited Task<UnitType> HandleAsync(...) signature. That breaks the MediatR-style “request without response” contract this PR is introducing and conflicts with the new docs that present IRequestHandler<TRequest> as the void/no-result variant. Mirror ICommandHandler<TCommand> here with a new Task HandleAsync(...) plus an explicit bridge back to IRequestHandler<TRequest, UnitType>.

Proposed API fix
 public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, UnitType> where TRequest : IRequest<UnitType>
 {
+    new Task HandleAsync(TRequest request, CancellationToken cancellationToken = default);
+
+    async Task<UnitType> IRequestHandler<TRequest, UnitType>.HandleAsync(
+        TRequest request,
+        CancellationToken cancellationToken)
+    {
+        await HandleAsync(request, cancellationToken);
+        return UnitType.Value;
+    }
 }

Based on learnings: Keep handler signatures as Task HandleAsync(T, CancellationToken) or Task<TResult> HandleAsync(T, CancellationToken) to ensure InProcessDispatcher reflection-based resolution succeeds; missing HandleAsync will throw InvalidOperationException.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, UnitType> where TRequest : IRequest<UnitType>
{
}
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, UnitType> where TRequest : IRequest<UnitType>
{
new Task HandleAsync(TRequest request, CancellationToken cancellationToken = default);
async Task<UnitType> IRequestHandler<TRequest, UnitType>.HandleAsync(
TRequest request,
CancellationToken cancellationToken)
{
await HandleAsync(request, cancellationToken);
return UnitType.Value;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProDispatch/Abstractions/Requests/IRequestHandler.cs` around lines 8 -
10, IRequestHandler<TRequest> currently inherits HandleAsync returning
Task<UnitType>, so implementers must return UnitType; change the contract to
expose a no-result Task by adding a new Task HandleAsync(TRequest request,
CancellationToken cancellationToken) declaration (use the new modifier) on
IRequestHandler<TRequest>, and provide a default bridge implementation of
IRequestHandler<TRequest, UnitType>.HandleAsync that calls the new HandleAsync
and returns UnitType (e.g. async Task<UnitType> IRequestHandler<TRequest,
UnitType>.HandleAsync(...) { await HandleAsync(...); return UnitType.Value; })
so reflection-based resolution still finds HandleAsync; reference
IRequestHandler<TRequest>, IRequestHandler<TRequest, UnitType>,
IRequest<UnitType>, UnitType, HandleAsync and mirror the pattern used by
ICommandHandler<TCommand>.

@hard-rox hard-rox merged commit c71bb3b into main Apr 16, 2026
3 of 4 checks passed
@hard-rox hard-rox deleted the mediator-like-interface branch April 16, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant