MediatR pipeline behaviors for validation, logging, performance monitoring, and transaction management. Trigger: pipeline behavior, IPipelineBehavior, validation behavior, logging behavior.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Delivers DB-free sandbox API regression tests for Next.js/Vitest to catch AI blind spots in self-reviewed code changes like API routes and backend logic.
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)