gRPC server and client interceptors for cross-cutting concerns. Covers exception mapping, culture switching, access claims extraction, and registration order. Trigger: gRPC interceptor, exception handling, culture, access claims.
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.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
ApplicationExceptionInterceptor maps domain exceptions to RpcException with ProblemDetailsThreadCultureInterceptor reads language header and sets thread cultureaccess-claims-bin metadata headernamespace {Company}.{Domain}.Grpc.Interceptors;
public sealed class ApplicationExceptionInterceptor(
ILogger<ApplicationExceptionInterceptor> logger) : 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)
{
logger.LogWarning(ex, "Domain exception: {Message}", ex.Message);
var problemDetails = provider.ToProblemDetails();
var metadata = new Metadata
{
{ "problem-details-bin",
Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(problemDetails)) }
};
throw new RpcException(
new Status(MapStatusCode(ex), ex.Message), metadata);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception in gRPC handler");
throw new RpcException(
new Status(StatusCode.Internal, "Internal server error"));
}
}
private static StatusCode MapStatusCode(DomainException ex) => ex switch
{
NotFoundException => StatusCode.NotFound,
ConflictException => StatusCode.AlreadyExists,
ValidationException => StatusCode.InvalidArgument,
UnauthorizedException => StatusCode.PermissionDenied,
_ => StatusCode.Internal
};
}
namespace {Company}.{Domain}.Grpc.Interceptors;
public sealed class ThreadCultureInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var languageHeader = context.RequestHeaders
.FirstOrDefault(h => h.Key == "language")?.Value;
if (!string.IsNullOrEmpty(languageHeader))
{
var culture = new CultureInfo(languageHeader);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
return await continuation(request, context);
}
}
namespace {Company}.{Domain}.Grpc.Interceptors;
public sealed class AccessClaimsInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var claimsEntry = context.RequestHeaders
.FirstOrDefault(h => h.Key == "access-claims-bin");
if (claimsEntry is not null)
{
var claimsJson = Encoding.UTF8.GetString(claimsEntry.ValueBytes);
var claims = JsonConvert.DeserializeObject<AccessClaims>(claimsJson);
context.UserState["AccessClaims"] = claims;
}
return await continuation(request, context);
}
}
namespace {Company}.{Domain}.Grpc.Interceptors;
public sealed class LanguageClientInterceptor : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var headers = context.Options.Headers ?? new Metadata();
headers.Add("language", CultureInfo.CurrentCulture.Name);
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method, context.Host,
context.Options.WithHeaders(headers));
return continuation(request, newContext);
}
}
builder.Services.AddGrpc(options =>
{
// Order matters: first registered = first executed
options.Interceptors.Add<ThreadCultureInterceptor>();
options.Interceptors.Add<AccessClaimsInterceptor>();
options.Interceptors.Add<ApplicationExceptionInterceptor>();
});
| Anti-Pattern | Correct Approach |
|---|---|
| Exception handling in every service method | Use exception interceptor |
| Culture setting in every handler | Use culture interceptor |
| Wrong interceptor order | Culture before exception interceptor |
| Exposing stack traces to clients | Log internally, return clean error to client |
# Find interceptors
grep -r ": Interceptor" --include="*.cs" src/
# Find interceptor registration
grep -r "Interceptors.Add" --include="*.cs" src/
# Find ProblemDetails in gRPC context
grep -r "problem-details-bin" --include="*.cs" src/