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.Service business 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 from Dpz.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.cs or MediatorExtensions.cs
  • Calling mediator.Send(...) from a Controller, a Dpz.Core.Service impl, 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.Service may inject IMediator and call mediator.Send(...).
  • Dpz.Core.Service.Mediator handlers must NOT reference any interface from Dpz.Core.Service (no IArticleService, IVideoService, etc.). If a handler needs persistence, inject IRepository<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

  1. Pick the right module folder under Features/. Create one if a new bounded context appears.
  2. Create the request class in Commands/ (write/side-effect) or Queries/ (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; }
}
  1. 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);
    }
}
  1. Done — registration is automatic. The Mediator source generator (Mediator.SourceGenerator 3.0.2) discovers IRequestHandler<,> / IRequestHandler<> implementations at compile time, and MediatorExtensions.AddDpzMediator() (called from Dpz.Core.Service.ServiceExtensions.AddBusinessServices) registers everything as Scoped. No services.AddScoped<IFooHandler, FooHandler>() line is required and none should be added.

  2. 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 repoUse 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:

  1. Common/Behaviors/MediatorLoggingBehavior — minimal start/stop logger (debug logs currently commented out; safe to leave as-is)
  2. Common/Behaviors/MediatorPerformanceBehaviorStopwatch-based timer, logs LogWarning for any handler ≥ 800 ms (SlowRequestThresholdMs)
  3. Common/Behaviors/MediatorExceptionWrappingBehavior — catches handler exceptions, logs LogError, and converts to ResponseResult.Fail / ResponseResult<T>.WithFail when 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 in AddDpzMediatorGovernance — order matters; outer behaviors run first.
  • Do not call services.AddMediator(...) again — that's already done in AddDpzMediator.

Coding conventions reminders

(Full project conventions in AGENTS.md; the most relevant for this folder:)

  • using directives are mostly globalized in Usings.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's Handle is the framework-mandated exception)
  • Always accept CancellationToken cancellationToken as 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 . from src/

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.*Service interface
  • Handler does not return a Public.Entity.* type
  • No manual services.AddScoped<...>() line added for the handler
  • If a new IPipelineBehavior was added, it is registered in AddDpzMediatorGovernance
  • dotnet build src/Dpz.Core.slnx passes (Mediator's source generator runs at build time — registration errors surface here)
  • dotnet csharpier . (run from src/) is clean
评论加载中...