원클릭으로
pipeline-behaviors
Use when adding cross-cutting MediatR pipeline behaviors for validation, logging, or transactions.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when adding cross-cutting MediatR pipeline behaviors for validation, logging, or transactions.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Use when adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
SOC 직업 분류 기준
| name | pipeline-behaviors |
| description | Use when adding cross-cutting MediatR pipeline behaviors for validation, logging, or transactions. |
| metadata | {"category":"cqrs","agent":"ef-specialist","when-to-use":"When creating MediatR pipeline behaviors for validation, logging, or transactions"} |
public sealed class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, ct)));
var failures = results
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
public sealed class LoggingBehavior<TRequest, TResponse>(
ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
logger.LogInformation(
"Handling {RequestName}: {@Request}",
requestName, request);
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
{
logger.LogWarning(
"Long-running request {RequestName}: {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
}
else
{
logger.LogInformation(
"Handled {RequestName} in {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
}
return response;
}
}
public sealed class PerformanceBehavior<TRequest, TResponse>(
ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private const int ThresholdMs = 500;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
if (sw.ElapsedMilliseconds > ThresholdMs)
{
logger.LogWarning(
"Slow request detected: {RequestName} took {ElapsedMs}ms. " +
"Request: {@Request}",
typeof(TRequest).Name,
sw.ElapsedMilliseconds,
request);
}
return response;
}
}
// Marker interface for transactional commands
public interface ITransactionalRequest { }
public sealed class TransactionBehavior<TRequest, TResponse>(
AppDbContext db,
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ITransactionalRequest
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var strategy = db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var transaction =
await db.Database.BeginTransactionAsync(ct);
logger.LogInformation(
"Begin transaction for {RequestName}",
typeof(TRequest).Name);
var response = await next();
await transaction.CommitAsync(ct);
logger.LogInformation(
"Committed transaction for {RequestName}",
typeof(TRequest).Name);
return response;
});
}
}
public sealed class UnhandledExceptionBehavior<TRequest, TResponse>(
ILogger<UnhandledExceptionBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
try
{
return await next();
}
catch (Exception ex)
{
logger.LogError(ex,
"Unhandled exception for {RequestName}: {@Request}",
typeof(TRequest).Name, request);
throw;
}
}
}
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
// Outermost → innermost
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(UnhandledExceptionBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(PerformanceBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(TransactionBehavior<,>));
});
// Register validators
builder.Services.AddValidatorsFromAssembly(
typeof(Program).Assembly);
Request arrives
→ UnhandledExceptionBehavior (catch + log)
→ LoggingBehavior (log entry/exit + timing)
→ ValidationBehavior (FluentValidation)
→ PerformanceBehavior (warn if slow)
→ TransactionBehavior (if ITransactionalRequest)
→ Handler (actual business logic)
IPipelineBehavior< implementationsAddBehavior calls in MediatR registrationBehaviors/ folder in the projectITransactionalRequest or similar marker interfacesValidationBehavior or LoggingBehavior classesITransactionalRequest)