ronnythedev/dotnet-clean-architecture-skills/10-dotnet-pipeline-behaviors

Claude Code and agent skill by ronnythedev/dotnet-clean-architecture-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Generates MediatR Pipeline Behaviors for cross-cutting concerns like logging, validation, exception handling, caching, and performance monitoring. Implements the decorator pattern around handlers.

Install

npx vskill@latest install ronnythedev/dotnet-clean-architecture-skills/10-dotnet-pipeline-behaviors

SKILL.md

MediatR Pipeline Behaviors

Overview

Pipeline Behaviors implement cross-cutting concerns that execute before/after every command or query handler:

  • Validation - Validate requests before handler executes
  • Logging - Log request/response details
  • Exception Handling - Convert exceptions to Results
  • Transaction - Wrap handlers in database transactions
  • Caching - Cache query results
  • Performance - Monitor slow operations

Quick Reference

BehaviorPurposeOrder
LoggingBehaviorLog requestsFirst (outer)
ValidationBehaviorValidate inputSecond
ExceptionHandlingBehaviorConvert exceptionsThird
TransactionBehaviorDatabase transactionFourth
CachingBehaviorCache responsesFifth (inner)

Behavior Structure

/Application/Abstractions/Behaviors/
├── LoggingBehavior.cs
├── ValidationBehavior.cs
├── ExceptionHandlingBehavior.cs
├── TransactionBehavior.cs
├── QueryCachingBehavior.cs
└── PerformanceBehavior.cs

Template: Logging Behavior

csharp
// src/{name}.application/Abstractions/Behaviors/LoggingBehavior.cs
using MediatR;
using Microsoft.Extensions.Logging;
using Serilog.Context;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Logs all requests and responses with timing information
/// </summary>
public sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
var requestId = Guid.NewGuid();
using (LogContext.PushProperty("RequestId", requestId))
using (LogContext.PushProperty("RequestName", requestName))
{
_logger.LogInformation(
"Handling {RequestName} ({RequestId})",
requestName,
requestId);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var response = await next();
stopwatch.Stop();
_logger.LogInformation(
"Handled {RequestName} ({RequestId}) in {ElapsedMs}ms",
requestName,
requestId,
stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Error handling {RequestName} ({RequestId}) after {ElapsedMs}ms",
requestName,
requestId,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
}

Template: Validation Behavior

csharp
// src/{name}.application/Abstractions/Behaviors/ValidationBehavior.cs
using FluentValidation;
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Validates requests using FluentValidation validators
/// Returns ValidationResult with errors instead of throwing
/// </summary>
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var errors = validationResults
.SelectMany(result => result.Errors)
.Where(failure => failure is not null)
.Select(failure => new Error(
failure.PropertyName,
failure.ErrorMessage))
.Distinct()
.ToArray();
if (errors.Length != 0)
{
return CreateValidationResult<TResponse>(errors);
}
return await next();
}
private static TResponse CreateValidationResult<TResponse>(Error[] errors)
{
// Handle Result type
if (typeof(TResponse) == typeof(Result))
{
return (TResponse)(object)ValidationResult.WithErrors(errors);
}
// Handle Result<T> type
var resultType = typeof(TResponse);
if (resultType.IsGenericType &&
resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
var valueType = resultType.GetGenericArguments()[0];
var validationResultType = typeof(ValidationResult<>).MakeGenericType(valueType);
var validationResult = Activator.CreateInstance(
validationResultType,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new object[] { errors },
null);
return (TResponse)validationResult!;
}
throw new InvalidOperationException(
$"Cannot create validation result for type {typeof(TResponse).Name}");
}
}

Template: Exception Handling Behavior

csharp
// src/{name}.application/Abstractions/Behaviors/ExceptionHandlingBehavior.cs
using MediatR;
using Microsoft.Extensions.Logging;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Catches unhandled exceptions and converts them to Result.Failure
/// </summary>
public sealed class ExceptionHandlingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
where TResponse : Result
{
private readonly ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> _logger;
public ExceptionHandlingBehavior(
ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
try
{
return await next();
}
catch (Exception ex)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(
ex,
"Unhandled exception for request {RequestName}",
requestName);
return CreateExceptionResult<TResponse>(ex);
}
}
private static TResponse CreateExceptionResult<TResponse>(Exception exception)
{
var error = new Error(
"Error.Unhandled",
exception.Message);
if (typeof(TResponse) == typeof(Result))
{
return (TResponse)(object)Result.Failure(error);
}
var resultType = typeof(TResponse);
if (resultType.IsGenericType &&
resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
var valueType = resultType.GetGenericArguments()[0];
var failureMethod = typeof(Result)
.GetMethod(nameof(Result.Failure), new[] { typeof(Error) })!
.MakeGenericMethod(valueType);
return (TResponse)failureMethod.Invoke(null, new object[] { error })!;
}
throw new InvalidOperationException(
$"Cannot create exception result for type {typeof(TResponse).Name}");
}
}

Template: Transaction Behavior

csharp
// src/{name}.application/Abstractions/Behaviors/TransactionBehavior.cs
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using {name}.application.abstractions.messaging;
using {name}.infrastructure;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Wraps command handlers in database transactions
/// Only applies to commands (write operations)
/// </summary>
public sealed class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommand<TResponse> // Only commands
{
private readonly ApplicationDbContext _dbContext;
private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;
public TransactionBehavior(
ApplicationDbContext dbContext,
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
{
_dbContext = dbContext;
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
// Check if already in a transaction
if (_dbContext.Database.CurrentTransaction is not null)
{
return await next();
}
await using var transaction = await _dbContext.Database
.BeginTransactionAsync(cancellationToken);
_logger.LogInformation(
"Beginning transaction for {RequestName}",
requestName);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
_logger.LogInformation(
"Committed transaction for {RequestName}",
requestName);
return response;
}
catch (Exception ex)
{
await transaction.RollbackAsync(cancellationToken);
_logger.LogError(
ex,
"Rolled back transaction for {RequestName}",
requestName);
throw;
}
}
}

Template: Query Caching Behavior

csharp
// src/{name}.application/Abstractions/Caching/ICachedQuery.cs
namespace {name}.application.abstractions.caching;
/// <summary>
/// Marker interface for queries that should be cached
/// </summary>
public interface ICachedQuery
{
string CacheKey { get; }
TimeSpan? CacheDuration { get; }
}
/// <summary>
/// Strongly-typed cached query
/// </summary>
public interface ICachedQuery<TResponse> : IQuery<TResponse>, ICachedQuery
{
}
csharp
// src/{name}.application/Abstractions/Behaviors/QueryCachingBehavior.cs
using MediatR;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using {name}.application.abstractions.caching;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Caches query results using distributed cache
/// Only applies to queries implementing ICachedQuery
/// </summary>
public sealed class QueryCachingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICachedQuery<TResponse>
where TResponse : class
{
private readonly IDistributedCache _cache;
private readonly ILogger<QueryCachingBehavior<TRequest, TResponse>> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public QueryCachingBehavior(
IDistributedCache cache,
ILogger<QueryCachingBehavior<TRequest, TResponse>> logger)
{
_cache = cache;
_logger = logger;
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
64
Repository forks
11
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ronnythedev/dotnet-clean-architecture-skills/10-dotnet-pipeline-behaviors/badge)](https://verified-skill.com/skills/ronnythedev/dotnet-clean-architecture-skills/10-dotnet-pipeline-behaviors)

Something wrong with this skill or its page? Tell us.