GrpcClientFactory registration with AddGrpcClient using (provider, client) callback, IOptions<ExternalServicesOptions> for URL resolution, and RetryCallerService wrapper for resilient gRPC calls with retry loop. Trigger: gRPC client, client factory, external service, cross-service call, retry.
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.
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 abandonment