skills/microservice/processor/grpc-client/SKILL.md
Use when calling external gRPC services from a processor with retry and client factory.
npx skillsauth add faysilalshareef/dotnet-ai-kit grpc-clientInstall 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.
AddGrpcClient<T>() with (provider, client) => callback to resolve URL from DIIOptions<ExternalServicesOptions> resolved inside the callbackExternalServicesOptions uses [Required, Url] attributes with const string Options patternRetryCallerService wraps gRPC calls with retry loop (catch RpcException, delay, retry)RpcException with StatusCode.AlreadyExists is treated as idempotent successRetryCallerServicenamespace {Company}.{Domain}.Processor.Setup;
public class ExternalServicesOptions
{
public const string Options = "ExternalServices";
[Required, Url]
public required string OrderCommand { get; init; }
[Required, Url]
public required string OrderQuery { get; init; }
[Required, Url]
public required string ProductGrpc { get; init; }
[Required, Url]
public required string ProductGrpcReplica { get; init; }
}
namespace {Company}.{Domain}.Processor.Setup;
public static class ExternalServicesRegistrationExtensions
{
public static IServiceCollection RegisterExternalServices(
this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ExternalServicesOptions>()
.Bind(configuration.GetSection(ExternalServicesOptions.Options));
// Pattern: AddGrpcClient with (provider, configure) callback
// URL is resolved from IOptions at runtime, not at registration time
services.AddGrpcClient<OrderCommands.OrderCommandsClient>((provider, configure) =>
{
var options = provider.GetRequiredService<IOptions<ExternalServicesOptions>>();
configure.Address = new Uri(options.Value.OrderCommand);
});
services.AddGrpcClient<OrderQueries.OrderQueriesClient>((provider, configure) =>
{
var options = provider.GetRequiredService<IOptions<ExternalServicesOptions>>();
configure.Address = new Uri(options.Value.OrderQuery);
});
services.AddGrpcClient<ProductQueries.ProductQueriesClient>((provider, configure) =>
{
var options = provider.GetRequiredService<IOptions<ExternalServicesOptions>>();
configure.Address = new Uri(options.Value.ProductGrpc);
});
// Some clients need explicit Action cast for overload resolution
services.AddGrpcClient<SpecialQueries.SpecialQueriesClient>(
(Action<IServiceProvider, global::Grpc.Net.ClientFactory.GrpcClientFactoryOptions>)
((provider, configure) =>
{
var options = provider
.GetRequiredService<IOptions<ExternalServicesOptions>>();
configure.Address = new Uri(options.Value.ProductGrpcReplica);
}));
return services;
}
}
namespace {Company}.{Domain}.Processor.Application.Contracts;
public interface IRetryCallerService
{
Task<T> CallAsync<T>(
Func<Task<T>> operation,
int retryCount = 5,
int millisecondsDelay = 250);
}
namespace {Company}.{Domain}.Processor.Infra.Services;
public class RetryCallerService : IRetryCallerService
{
private readonly ILogger<RetryCallerService> _logger;
public RetryCallerService(ILogger<RetryCallerService> logger)
{
_logger = logger;
}
public async Task<T> CallAsync<T>(
Func<Task<T>> operation,
int retryCount = 5,
int millisecondsDelay = 250)
{
var count = retryCount + 1;
while (true)
{
count--;
try
{
return await operation();
}
catch (RpcException e)
{
_logger.LogWarning(e, "Call failed with {attempts} left", count);
if (count == 0)
throw new RpcException(
new Status(StatusCode.Aborted, e.Message));
}
await Task.Delay(millisecondsDelay);
}
}
}
namespace {Company}.{Domain}.Processor.Application.Features.OrderEvents;
public class OrderCreatedHandler(
OrderCommands.OrderCommandsClient commandsClient,
OrderQueries.OrderQueriesClient queriesClient)
: IRequestHandler<Event<OrderCreatedData>, bool>
{
public async Task<bool> Handle(
Event<OrderCreatedData> @event, CancellationToken ct)
{
try
{
await commandsClient.CreateOrderAsync(new CreateOrderRequest
{
OrderId = @event.AggregateId.ToString(),
Sequence = @event.Sequence
}, cancellationToken: ct);
return true;
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
{
// Idempotent -- already processed
return true;
}
catch (Exception)
{
throw; // Let listener abandon the message for retry
}
}
}
// In ServiceBusRegistrationExtensions or ServicesRegistrationExtensions
services.AddSingleton<IRetryCallerService, RetryCallerService>();
{
"ExternalServices": {
"OrderCommand": "https://order-command:443",
"OrderQuery": "https://order-query:443",
"ProductGrpc": "https://product-grpc:443",
"ProductGrpcReplica": "https://product-grpc-replica:443"
}
}
| Anti-Pattern | Correct Approach |
|---|---|
| Resolving options at registration time | Use (provider, configure) callback to resolve at runtime |
| Using Polly for retry | Use custom RetryCallerService with while loop |
| Not handling AlreadyExists | Treat RpcException with AlreadyExists as idempotent success |
| Hardcoded service URLs | Use ExternalServicesOptions from configuration |
| Creating gRPC channels manually | Use AddGrpcClient factory pattern |
| Catching and swallowing exceptions | Re-throw to let listener abandon the message |
# Find AddGrpcClient registrations
grep -r "AddGrpcClient<" --include="*.cs" src/
# Find ExternalServicesOptions
grep -r "ExternalServicesOptions" --include="*.cs" src/
# Find RetryCallerService usage
grep -r "RetryCallerService\|IRetryCallerService" --include="*.cs" src/
# Find RpcException handling
grep -r "RpcException" --include="*.cs" src/
# Find AlreadyExists idempotent handling
grep -r "StatusCode.AlreadyExists" --include="*.cs" src/
ExternalServicesOptions with [Required, Url]AddGrpcClient<T> with (provider, configure) callback in registrationStatusCode.AlreadyExists as idempotent success in handlersRetryCallerService.CallAsync for operations needing retry semantics beyond message abandonmentdata-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.