coder-csharp-error-handling
npx skills add https://github.com/ozerohax/assistagents --skill coder-csharp-error-handling
Agent 安装分布
Skill 文档
<skill_overview> Implement robust error handling following modern .NET patterns Handling exceptions in services Implementing validation logic Designing API error responses Choosing Result pattern vs exceptions Setting up global exception handling Microsoft Exception Handling </skill_overview> <exceptions_vs_result> <use_exceptions_when> Truly exceptional, unexpected failures System-level errors (I/O, network, hardware) In constructors (can’t return Result) Framework boundaries expecting exceptions Programming errors (null, out of range) </use_exceptions_when> <use_result_when> Expected business failures (validation, not found) Multiple possible failure modes Performance-critical hot paths Domain layer business logic Compile-time safety for error handling </use_result_when> <example_comparison> // Exception: Unexpected failure if (connection == null) throw new InvalidOperationException(“Connection not initialized”); // Result: Expected business outcome public Result<User> GetUser(int id) { var user = _repository.Find(id); if (user == null) return Result.Fail(“User not found”); // Expected case return Result.Ok(user); } </example_comparison> </exceptions_vs_result> <exception_best_practices> Throw for truly exceptional conditions only Use specific exception types Include context in exception message // Good: Specific exception with context throw new ArgumentNullException(nameof(email), “Email is required for user creation”); // Good: Use built-in guard methods (.NET 6+) ArgumentNullException.ThrowIfNull(user); ArgumentException.ThrowIfNullOrEmpty(email); // Bad: Generic exception throw new Exception(“Error occurred”); Catch specific exceptions first, general last Use exception filters (when clause) for fine-grained control Always use “throw;” not “throw ex;” to preserve stack trace try { await ProcessOrderAsync(order); } catch (ValidationException ex) { _logger.LogWarning(ex, “Validation failed for order {OrderId}”, order.Id); return BadRequest(ex.Errors); } catch (NotFoundException ex) { return NotFound(ex.Message); } catch (Exception ex) when (ex is TimeoutException or HttpRequestException) { _logger.LogWarning(ex, “Transient error, will retry”); throw; // Preserve stack trace! } catch (Exception ex) { _logger.LogError(ex, “Unexpected error processing order {OrderId}”, order.Id); throw; // Never “throw ex;” } <custom_exceptions> public class OrderProcessingException : Exception { public string OrderId { get; } public string ErrorCode { get; }
public OrderProcessingException(string orderId, string message, string errorCode)
: base(message)
{
OrderId = orderId;
ErrorCode = errorCode;
}
public OrderProcessingException(string orderId, string message, Exception inner)
: base(message, inner)
{
OrderId = orderId;
}
} </custom_exceptions> </exception_best_practices> <result_pattern> Rich API, multiple errors, chaining Clean fluent API, functional style Discriminated unions, exhaustive matching <erroror_example> public async Task<ErrorOr<Order>> CreateOrderAsync(CreateOrderCommand cmd) { if (cmd.Items.Count == 0) return Error.Validation(“Order must have items”);
var customer = await _customerRepo.FindAsync(cmd.CustomerId);
if (customer == null)
return Error.NotFound("Customer not found");
if (!customer.IsActive)
return Error.Validation("Customer account is inactive");
var order = new Order(cmd);
await _orderRepo.AddAsync(order);
return order;
} // Controller usage [HttpPost] public async Task<IActionResult> CreateOrder(CreateOrderDto dto) { var result = await _orderService.CreateOrderAsync(dto.ToCommand());
return result.Match(
order => Ok(new OrderDto(order)),
errors => Problem(errors.First().Description)
);
} </erroror_example> public async Task<ErrorOr<OrderResult>> ProcessOrderAsync(int orderId) { return await ValidateOrderAsync(orderId) .Then(order => CalculatePriceAsync(order)) .Then(order => ProcessPaymentAsync(order)) .Then(order => SendConfirmationAsync(order)); } </result_pattern> Powerful, fluent validation with separation of concerns public class CreateUserValidator : AbstractValidator<CreateUserDto> { public CreateUserValidator(IUserRepository repository) { RuleFor(x => x.Email) .NotEmpty().WithMessage(“Email is required”) .EmailAddress().WithMessage(“Invalid email format”) .MustAsync(async (email, ct) => !await repository.ExistsAsync(email)) .WithMessage(“Email already registered”);
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.Matches("[A-Z]").WithMessage("Must contain uppercase")
.Matches("[a-z]").WithMessage("Must contain lowercase")
.Matches("[0-9]").WithMessage("Must contain number");
RuleFor(x => x.Age)
.InclusiveBetween(18, 120);
}
} <data_annotations> Simple validation with attributes <use_for>DTOs, simple input validation</use_for> public class CreateUserDto { [Required] [EmailAddress] public string Email { get; init; }
[Required]
[MinLength(8)]
public string Password { get; init; }
[Range(18, 120)]
public int Age { get; init; }
} </data_annotations> <validation_layers> Input format validation (DataAnnotations) Business rules (FluentValidation) Invariants (guard clauses, exceptions) </validation_layers> <aspnetcore_error_handling> Global exception handling (.NET 8+) public class GlobalExceptionHandler : IExceptionHandler { private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken ct)
{
_logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
var problemDetails = exception switch
{
ValidationException ex => new ProblemDetails
{
Type = "https://example.com/validation-error",
Title = "Validation Error",
Status = 400,
Detail = ex.Message
},
NotFoundException => new ProblemDetails
{
Type = "https://example.com/not-found",
Title = "Not Found",
Status = 404
},
_ => new ProblemDetails
{
Type = "https://example.com/internal-error",
Title = "Internal Server Error",
Status = 500,
Detail = "An unexpected error occurred"
}
};
problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;
httpContext.Response.StatusCode = problemDetails.Status ?? 500;
await httpContext.Response.WriteAsJsonAsync(problemDetails, ct);
return true;
}
} // Registration builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); app.UseExceptionHandler(); <problem_details> RFC 7807 standard error format { “type”: “https://example.com/validation-error“, “title”: “Validation Error”, “status”: 400, “detail”: “Email is required”, “instance”: “/api/users”, “traceId”: “00-abc123…” } </problem_details> </aspnetcore_error_handling> <logging_errors> Always include exception object: LogError(ex, “message”) Add context: orderId, userId, operation name Use structured logging with templates Include correlation ID for tracing try { await ProcessOrderAsync(order); } catch (Exception ex) { _logger.LogError(ex, “Failed to process order {OrderId} for customer {CustomerId}. Amount: {Amount}”, order.Id, order.CustomerId, order.TotalAmount); throw; } <never_log> Passwords (even hashed) Credit card numbers API keys and tokens Personal identification numbers </never_log> </logging_errors> <anti_patterns> catch (Exception) { } Swallows errors silently catch (Exception ex) { throw new Exception(“Error”, ex); } Loses original exception type throw ex; Resets stack trace throw; Using exceptions for expected business logic Expensive, unclear intent Use Result pattern for expected failures </anti_patterns>