skills/microservice/grpc/interceptors/SKILL.md
Use when adding gRPC interceptors for exception mapping, culture switching, or claims extraction.
npx skillsauth add faysilalshareef/dotnet-ai-kit interceptorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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/
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.