Conversation
…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.
…d notification handling
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughProDispatch now supports MediatR-style Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
src/ProDispatch/Dispatcher/InProcessDispatcher.cs (2)
35-43: Consider addingTryGetInstanceto 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: UnusedresponseTypeparameter -typeof(TResponse)is already available.The
responseTypeparameter is always passed astypeof(TResponse)from the caller. SinceTResponseis a generic type parameter, you can usetypeof(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
📒 Files selected for processing (27)
.github/copilot-instructions.mdDirectory.Packages.propsREADME.mdbenchmarks/ProDispatch.Benchmarks/DispatcherBenchmarks.csbenchmarks/ProDispatch.Benchmarks/ProDispatch.Benchmarks.csprojdocs/advanced-usage.mddocs/getting-started.mddocs/migration-from-mediatr.mdsamples/ProDispatch.Examples.Console/Behaviors/CommandOnlyBehavior.cssamples/ProDispatch.Examples.Console/Behaviors/QueryOnlyBehavior.cssamples/ProDispatch.Examples.Console/Program.cssamples/ProDispatch.Examples.MinimalApi/Behaviors/CommandOnlyBehavior.cssamples/ProDispatch.Examples.MinimalApi/Behaviors/QueryOnlyBehavior.cssamples/ProDispatch.Examples.MinimalApi/Program.cssrc/ProDispatch/Abstractions/Commands/ICommand.cssrc/ProDispatch/Abstractions/Commands/ICommandHandler.cssrc/ProDispatch/Abstractions/Dispatcher/IDispatcher.cssrc/ProDispatch/Abstractions/Dispatcher/IMediator.cssrc/ProDispatch/Abstractions/Pipeline/ICommandPipelineBehavior.cssrc/ProDispatch/Abstractions/Pipeline/IQueryPipelineBehavior.cssrc/ProDispatch/Abstractions/Pipeline/PipelineScope.cssrc/ProDispatch/Abstractions/Queries/IQuery.cssrc/ProDispatch/Abstractions/Queries/IQueryHandler.cssrc/ProDispatch/Abstractions/Requests/IRequest.cssrc/ProDispatch/Abstractions/Requests/IRequestHandler.cssrc/ProDispatch/Dispatcher/InProcessDispatcher.cssrc/ProDispatch/GlobalUsings.cs
| 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()); |
There was a problem hiding this comment.
Benchmark the typed Unit path separately from the compatibility path.
These registrations force ProDispatch through the dispatcher’s object→Unit 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.
| - 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). |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| ```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<,>()); |
There was a problem hiding this comment.
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.
| public class GetUserQueryHandler : IRequestHandler<GetUserQuery, User> | ||
| { | ||
| public Task<User> HandleAsync(GetUserQuery request, CancellationToken cancellationToken) | ||
| { | ||
| // ... fetch user | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| [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 |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/jbogard/MediatR/blob/master/src/MediatR/IMediator.cs
- 2: https://github.com/jbogard/MediatR/blob/master/src/MediatR/IPublisher.cs
- 3: https://github.com/LuckyPennySoftware/MediatR/blob/master/src/MediatR/IMediator.cs
- 4: https://github.com/jbogard/MediatR/blob/master/src/MediatR/ISender.cs
- 5: https://github.com/LuckyPennySoftware/MediatR/blob/main/src/MediatR/IPublisher.cs
🏁 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 2Repository: 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 -100Repository: 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).
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for usages of PipelineScope enum
echo "=== Searching for PipelineScope usage ==="
rg -n "PipelineScope" --type=csRepository: 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).
| public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, UnitType> where TRequest : IRequest<UnitType> | ||
| { | ||
| } |
There was a problem hiding this comment.
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.
| 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>.
Summary
Describe the change.
Testing
Risks
List any risks, migrations, or breaking changes.
Summary by CodeRabbit
New Features
Documentation
Benchmarks
Samples