name: mediator description: Use when adding, modifying, or calling Mediator requests/handlers in the Dpz.Core.Service.Mediator project (CQRS-style Commands/Queries/Events under Features//, governed by logging/performance/exception-wrapping pipeline behaviors). Triggers on keywords like IRequest, IRequestHandler, IPipelineBehavior, IMediator, mediator.Send, ResponseResult, Features/Article|Markdown|Media|Health|Search|Storage|Code|Video, AddDpzMediator, AddDpzMediatorGovernance, MediatorLoggingBehavior, MediatorPerformanceBehavior, MediatorExceptionWrappingBehavior. license: MIT compatibility: opencode metadata: audience: contributors project: Dpz.Core.Service.Mediator package: Mediator (martinothamar) 3.0.2
What I do
- Guide adding a new Mediator request + handler in the correct Feature folder
- Enforce dependency direction: Mediator handlers may NOT depend on
Dpz.Core.Servicebusiness interfaces - Make sure handlers inject only
IRepository<>,IFusionCache,IMessagePublisher<>,IMapper,IMediator, loggers, and other infra primitives - Keep return types as DTO/ViewModel/
ResponseResult[<T>], never entities fromDpz.Core.Public.Entity - Remind you that registration is automatic via Mediator's source generator +
AddDpzMediator()— do NOT hand-register handlers - Verify that any new pipeline behavior is wired in
MediatorExtensions.AddDpzMediatorGovernance
When to use me
Use this skill whenever you are:
- Creating or editing files under
src/Dpz.Core.Service.Mediator/Features/** - Touching
Common/Behaviors/Mediator*Behavior.csorMediatorExtensions.cs - Calling
mediator.Send(...)from a Controller, aDpz.Core.Serviceimpl, or another handler and unsure which request type to use / create - Reviewing a PR that adds a request/handler and you need to check conventions
Skip this skill for: pure DI service work in Dpz.Core.Service (use the project's RegisterInject config instead), RabbitMQ message handlers (those live under Dpz.Core.WebApi/Services/Handlers and Dpz.Core.MessageQueue), or controllers that merely call an existing request.
Architecture cheat sheet
Dependency direction (do not violate):
Data Access (MongodbAccess) -> Mediator -> Service -> Application (Web / WebApi / Auth / Web.Jobs)
Dpz.Core.Servicemay injectIMediatorand callmediator.Send(...).Dpz.Core.Service.Mediatorhandlers must NOT reference any interface fromDpz.Core.Service(noIArticleService,IVideoService, etc.). If a handler needs persistence, injectIRepository<TEntity>directly.- Handlers freely use:
IRepository<>,IFusionCache,IMessagePublisher<TMessage>,IMapper,IMediator,IHttpClientFactory,IConfiguration,ILogger<>.
Folder layout (one feature per top-level folder):
Features/
<Module>/ # Article, Markdown, Media, Health, Search, Storage, Code, Video, ...
Commands/ # write-side: IRequest / IRequest<TResponse>
Queries/ # read-side: IRequest<TResponse>
Events/ # (when needed) notifications via INotification
Contracts/ # request-local DTOs that don't belong in Public.ViewModel
Request and its handler live side by side in the same folder. Existing convention names handlers either XxxHandler or XxxEvent — both are acceptable; prefer XxxHandler for new code.
Authoring a new request + handler
- Pick the right module folder under
Features/. Create one if a new bounded context appears. - Create the request class in
Commands/(write/side-effect) orQueries/(pure read):
namespace Dpz.Core.Service.Mediator.Features.<Module>.Queries;
/// <summary>
/// 中文 XML 注释(仓库约定,所有 public 成员都要写)
/// </summary>
public class FooReadRequest : IRequest<FooResponse?>
{
public required string Id { get; set; }
}
- Create the handler in the same folder. Use primary constructors (single ctor convention):
namespace Dpz.Core.Service.Mediator.Features.<Module>.Queries;
public class FooReadHandler(
IRepository<Public.Entity.Foo> repository,
IMapper mapper,
ILogger<FooReadHandler> logger
) : IRequestHandler<FooReadRequest, FooResponse?>
{
public async ValueTask<FooResponse?> Handle(
FooReadRequest request,
CancellationToken cancellationToken
)
{
if (!ObjectId.TryParse(request.Id, out var oid))
{
return null;
}
var entity = await repository.FindAsync(oid, cancellationToken);
return entity is null ? null : mapper.Map<FooResponse>(entity);
}
}
Done — registration is automatic. The Mediator source generator (
Mediator.SourceGenerator3.0.2) discoversIRequestHandler<,>/IRequestHandler<>implementations at compile time, andMediatorExtensions.AddDpzMediator()(called fromDpz.Core.Service.ServiceExtensions.AddBusinessServices) registers everything asScoped. Noservices.AddScoped<IFooHandler, FooHandler>()line is required and none should be added.Call it from a Controller / Service:
public class XController(IMediator mediator) : ControllerBase
{
public async Task<IActionResult> Get(string id, CancellationToken ct)
{
var result = await mediator.Send(new FooReadRequest { Id = id }, ct);
return result is null ? NotFound() : Ok(result);
}
}
Return type rules
Per README.md §3 the canonical return type for cross-boundary requests is Dpz.Core.Entity.Base.ResponseResult / ResponseResult<T>. The actual codebase relaxes this — current handlers return:
| Pattern in repo | Use it for |
|---|---|
IRequest<ResponseResult<T>> | Public-facing API endpoints where the caller maps to HTTP — required for new WebApi flows. The exception-wrapping pipeline auto-wraps thrown exceptions into ResponseResult.Fail(message, 500) only for ResponseResult / ResponseResult<> responses. |
IRequest<TResponse?> (DTO) | Read queries returning a single VM (ArticleReadHandler) |
IRequest<List<TResponse>> | Search/list queries (ArticleSearchEvent, ContentSearchEvent) |
IRequest (no payload, returns Unit) | Fire-and-forget commands (HealthCheckRequest, deprecated DeleteMarkdownRequest) |
Hard rule (do not break): never return an Public.Entity.* type from a handler. Map to a ViewModel/Response in Dpz.Core.Public.ViewModel (or a Feature-local DTO under Features/<Module>/Contracts/) before returning.
If you want auto exception → ResponseResult.Fail wrapping at the API boundary, choose ResponseResult / ResponseResult<T> as the response type — that is the only shape MediatorExceptionWrappingBehavior knows how to wrap; other shapes will rethrow.
Pipeline (governance) behaviors
Currently registered, in order, by MediatorExtensions.AddDpzMediatorGovernance:
Common/Behaviors/MediatorLoggingBehavior— minimal start/stop logger (debug logs currently commented out; safe to leave as-is)Common/Behaviors/MediatorPerformanceBehavior—Stopwatch-based timer, logsLogWarningfor any handler ≥ 800 ms (SlowRequestThresholdMs)Common/Behaviors/MediatorExceptionWrappingBehavior— catches handler exceptions, logsLogError, and converts toResponseResult.Fail/ResponseResult<T>.WithFailwhen the response type permits; otherwise rethrows
When adding a new behavior:
- Place it in
Common/Behaviors/ - Implement
IPipelineBehavior<TMessage, TResponse> where TMessage : IMessage - Add an
AddScoped(typeof(IPipelineBehavior<,>), typeof(YourBehavior<,>))line inAddDpzMediatorGovernance— order matters; outer behaviors run first. - Do not call
services.AddMediator(...)again — that's already done inAddDpzMediator.
Coding conventions reminders
(Full project conventions in AGENTS.md; the most relevant for this folder:)
usingdirectives are mostly globalized inUsings.cs(Mediator,MongoDB.Driver,Dpz.Core.Public.Entity,Dpz.Core.Public.ViewModel,Dpz.Core.Entity.Base,IFusionCache,IMapper, etc.) — do not re-import these per file- File-scoped namespaces, 4-space indent, max line 100, braces always required
- Async methods end in
Async(handler'sHandleis the framework-mandated exception) - Always accept
CancellationToken cancellationTokenas the last parameter and propagate it - Structured logging only:
logger.LogInformation("收到搜索请求: {Keyword}", request.Keyword);— never string-interpolate inside the message template - Return empty collections, never
null, for list/array returns - Format with CSharpier before committing:
dotnet csharpier .fromsrc/
Verification checklist before declaring done
- New request and handler are in
Features/<Module>/{Commands|Queries|Events}/with matching namespace - Handler does not reference any
Dpz.Core.Service.*Serviceinterface - Handler does not return a
Public.Entity.*type - No manual
services.AddScoped<...>()line added for the handler - If a new
IPipelineBehaviorwas added, it is registered inAddDpzMediatorGovernance -
dotnet build src/Dpz.Core.slnxpasses (Mediator's source generator runs at build time — registration errors surface here) -
dotnet csharpier .(run fromsrc/) is clean