skills/cqrs/pipeline-behaviors/SKILL.md
Use when adding cross-cutting MediatR pipeline behaviors for validation, logging, or transactions.
npx skillsauth add faysilalshareef/dotnet-ai-kit pipeline-behaviorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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)data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.