一键导入
error-handling
Use when implementing error handling with domain exceptions, ProblemDetails, or RpcException mapping.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when implementing error handling with domain exceptions, ProblemDetails, or RpcException mapping.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
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.
| name | error-handling |
| description | Use when implementing error handling with domain exceptions, ProblemDetails, or RpcException mapping. |
| metadata | {"category":"core","agent":"dotnet-architect","alwaysApply":true} |
DomainException base classRpcException with StatusCode and Trailersnamespace {Company}.{Domain}.Domain.Exceptions;
public abstract class DomainException(
string message,
string errorCode,
Exception? innerException = null) : Exception(message, innerException)
{
public string ErrorCode { get; } = errorCode;
}
public class NotFoundException(
string entityName,
object entityId)
: DomainException(
$"{entityName} with ID '{entityId}' was not found.",
$"{entityName.ToUpperInvariant()}_NOT_FOUND")
{
public string EntityName { get; } = entityName;
public object EntityId { get; } = entityId;
}
public class ValidationException(
string message,
string errorCode,
IDictionary<string, string[]>? errors = null)
: DomainException(message, errorCode)
{
public IDictionary<string, string[]> Errors { get; } =
errors ?? new Dictionary<string, string[]>();
}
public class ConcurrencyException(
string entityName,
object entityId)
: DomainException(
$"Concurrency conflict on {entityName} with ID '{entityId}'.",
$"{entityName.ToUpperInvariant()}_CONCURRENCY_CONFLICT")
{
public string EntityName { get; } = entityName;
public object EntityId { get; } = entityId;
}
Use uppercase snake_case constants scoped to the domain:
public static class ErrorCodes
{
public const string DrawNotFound = "DRAW_NOT_FOUND";
public const string InvalidSequence = "INVALID_SEQUENCE";
public const string DuplicateEntry = "DUPLICATE_ENTRY";
public const string InsufficientBalance = "INSUFFICIENT_BALANCE";
public const string OrderAlreadyCancelled = "ORDER_ALREADY_CANCELLED";
}
using Microsoft.AspNetCore.Http;
namespace {Company}.{Domain}.Domain.Exceptions;
public interface IProblemDetailsProvider
{
string Type { get; }
string Title { get; }
int Status { get; }
string Detail { get; }
}
public class NotFoundException : DomainException, IProblemDetailsProvider
{
// ... constructor and properties as above ...
public string Type => "https://tools.ietf.org/html/rfc9110#section-15.5.5";
public string Title => "Not Found";
public int Status => StatusCodes.Status404NotFound;
public string Detail => Message;
}
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"status": 404,
"detail": "Draw with ID '3fa85f64-5717-4562-b3fc-2c963f66afa6' was not found.",
"instance": "/api/draws/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"errorCode": "DRAW_NOT_FOUND"
}
using Grpc.Core;
using System.Text.Json;
namespace {Company}.{Domain}.Grpc.Interceptors;
public class ApplicationExceptionInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (DomainException ex) when (ex is IProblemDetailsProvider provider)
{
var metadata = new Metadata();
var problemDetails = new
{
provider.Type,
provider.Title,
provider.Status,
provider.Detail,
ex.ErrorCode
};
metadata.Add("problem-details-bin",
JsonSerializer.SerializeToUtf8Bytes(problemDetails));
throw new RpcException(
new Status(MapToGrpcStatusCode(provider.Status), ex.Message),
metadata);
}
}
private static StatusCode MapToGrpcStatusCode(int httpStatus) => httpStatus switch
{
400 => StatusCode.InvalidArgument,
404 => StatusCode.NotFound,
409 => StatusCode.AlreadyExists,
422 => StatusCode.InvalidArgument,
_ => StatusCode.Internal
};
}
Key details:
DomainException that implements IProblemDetailsProviderproblem-details-bin)StatusCode.Internal// In Program.cs or GrpcContainer
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<ApplicationExceptionInterceptor>();
});
public async Task<bool> Handle(
Event<OrderCreatedData> @event,
CancellationToken cancellationToken)
{
try
{
// ... handler logic ...
}
catch (DomainException ex)
{
_logger.LogError(ex,
"Domain error processing event {EventType} for aggregate {AggregateId}. " +
"ErrorCode: {ErrorCode}, UserId: {UserId}",
@event.Type,
@event.AggregateId,
ex.ErrorCode,
@event.UserId);
throw;
}
}
Always include in structured log context:
AggregateId — which entity was affectedUserId — who triggered the operationEventType — what operation was attemptedErrorCode — machine-readable error identifier| Exception Type | HTTP Status | gRPC StatusCode |
|---|---|---|
NotFoundException | 404 | NotFound |
ValidationException | 400 | InvalidArgument |
ConcurrencyException | 409 | AlreadyExists |
DomainException (other) | 422 | InvalidArgument |
Unhandled Exception | 500 | Internal |
| Anti-Pattern | Correct Approach |
|---|---|
Catching generic Exception and swallowing | Catch specific types, log, re-throw or convert |
| String error messages without codes | Use structured ErrorCode constants |
Throwing RpcException from domain layer | Throw DomainException, let interceptor convert |
| Generic "An error occurred" responses | Return specific ProblemDetails with error code |
| Logging without context properties | Include aggregateId, userId, eventType in every log |
Domain/Exceptions/IProblemDetailsProvider implementationsGrpc/Interceptors/