Service registration for gateway gRPC clients. Covers ServicesURLsOptions with validated properties, AddGrpcClient with RegisterUrl helper, RegisterInterceptors, and AddServicesURLsOptions pattern. Trigger: gateway registration, gRPC client factory, service URL, ValidateOnStart.
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.
ServicesURLsOptions holds all backend service URLs as [Required, Url] propertiesAddGrpcClients extension method registers all gRPC clientsAddServicesURLsOptions binds configuration, validates annotations, and validates on startAddGrpcClient<T>((provider, options) => RegisterUrl(...)) patternRegisterUrl resolves IOptions<ServicesURLsOptions> from the provider and calls RegisterInterceptorsRegisterInterceptors sets the Address and adds a GlobalMetadataInterceptor for culture/claims forwarding.csproj with GrpcServices="Client"Each backend service URL is a [Required, Url] property. Multiple services may share the same URL (e.g., commands and queries on the same host).
using System.ComponentModel.DataAnnotations;
namespace {Company}.Gateways.{Domain}.GrpcClients;
public class ServicesURLsOptions
{
public const string ServicesURLs = "ServicesURLs";
[Required, Url]
public required string OrderCommand { get; set; }
[Required, Url]
public required string OrderQuery { get; set; }
[Required, Url]
public required string ProductCommand { get; set; }
[Required, Url]
public required string ProductQuery { get; set; }
[Required, Url]
public required string ReportQueries { get; set; }
[Required, Url]
public required string FileStorage { get; set; }
}
The static extension method registers all gRPC clients. The AddServicesURLsOptions helper binds the configuration section and enables startup validation. The RegisterUrl helper resolves the options from DI and delegates to RegisterInterceptors.
using {Company}.Gateways.Common.Extensions;
using Grpc.Net.ClientFactory;
using Microsoft.Extensions.Options;
namespace {Company}.Gateways.{Domain}.GrpcClients;
public static class GrpcClientRegistrationExtension
{
public static void AddGrpcClients(
this IServiceCollection services, IConfiguration configuration)
{
AddServicesURLsOptions(services, configuration);
// Order Commands
services.AddGrpcClient<OrderCommands.OrderCommandsClient>(
(provider, options) =>
RegisterUrl(provider, o => o.OrderCommand, options));
// Order Queries
services.AddGrpcClient<OrderManagementQueries.OrderManagementQueriesClient>(
(provider, options) =>
RegisterUrl(provider, o => o.OrderQuery, options));
// Product Commands
services.AddGrpcClient<ProductCommands.ProductCommandsClient>(
(provider, options) =>
RegisterUrl(provider, o => o.ProductCommand, options));
// Product Queries (multiple clients sharing same URL)
services.AddGrpcClient<ProductQueries.ProductQueriesClient>(
(provider, options) =>
RegisterUrl(provider, o => o.ProductQuery, options));
services.AddGrpcClient<ProductReports.ProductReportsClient>(
(provider, options) =>
RegisterUrl(provider, o => o.ProductQuery, options));
// File Storage
services.AddGrpcClient<FileStorageUploader.FileStorageUploaderClient>(
(provider, options) =>
RegisterUrl(provider, o => o.FileStorage, options));
}
private static void AddServicesURLsOptions(
IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ServicesURLsOptions>()
.Bind(configuration.GetSection(ServicesURLsOptions.ServicesURLs))
.ValidateDataAnnotations()
.ValidateOnStart();
}
private static void RegisterUrl(
IServiceProvider provider,
Func<ServicesURLsOptions, string> getUrl,
GrpcClientFactoryOptions options)
{
var servicesURLs = provider
.GetRequiredService<IOptions<ServicesURLsOptions>>().Value;
options.RegisterInterceptors(getUrl(servicesURLs));
}
}
This lives in the shared {Company}.Gateways.Common library. It sets the gRPC channel address and adds a GlobalMetadataInterceptor that forwards culture and access claims to every gRPC call.
using Grpc.Core;
using Grpc.Core.Interceptors;
using Grpc.Net.ClientFactory;
using System.Globalization;
namespace {Company}.Gateways.Common.Extensions;
public static class GrpcClientRegistrationExtension
{
public static void RegisterInterceptors(
this GrpcClientFactoryOptions options, string address)
{
options.Address = new Uri(address);
options.InterceptorRegistrations.Add(
new InterceptorRegistration(
InterceptorScope.Client,
provider => new GlobalMetadataInterceptor(provider)));
}
class GlobalMetadataInterceptor(IServiceProvider provider) : Interceptor
{
private readonly IServiceProvider _provider = provider;
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var culture = CultureInfo.CurrentCulture;
var options = context.Options.WithHeaders(
[
new("language", culture.Name)
]);
var claimsProvider = _provider
.GetRequiredService<AccessClaimsProvider>();
var accessClaims = claimsProvider.GetAccessClaims();
if (accessClaims != null)
options.Headers!.Add("access-claims-bin", accessClaims);
return base.AsyncUnaryCall(
request,
new ClientInterceptorContext<TRequest, TResponse>(
context.Method, context.Host, options),
continuation);
}
}
}
<ItemGroup>
<Protobuf Include="Protos\order-commands.proto" GrpcServices="Client" />
<Protobuf Include="Protos\order-queries.proto" GrpcServices="Client" />
<Protobuf Include="Protos\product-commands.proto" GrpcServices="Client" />
<Protobuf Include="Protos\product-queries.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Net.ClientFactory" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup>
{
"ServicesURLs": {
"OrderCommand": "https://order-command:8081",
"OrderQuery": "https://order-query:8081",
"ProductCommand": "https://product-command:8081",
"ProductQuery": "https://product-query:8081",
"ReportQueries": "https://report-query:8081",
"FileStorage": "https://file-storage:8081"
}
}
builder.Services.AddGrpcClients(builder.Configuration);
| Anti-Pattern | Correct Approach |
|---|---|
| Hardcoded URLs in code | Use ServicesURLsOptions from configuration |
Missing ValidateOnStart | Fail fast on misconfigured URLs at startup |
| Resolving URLs from config at registration time | Use (provider, options) => callback to resolve at runtime |
| Missing interceptors on gRPC clients | Use RegisterInterceptors to add language/claims forwarding |
No [Required, Url] on URL properties | Always validate with data annotations |
| Creating new options class per client | Share one ServicesURLsOptions with all URLs |
# Find service URL options
grep -r "ServicesURLsOptions\|ServicesURLs" --include="*.cs"
# Find AddGrpcClient registrations
grep -r "AddGrpcClient<" --include="*.cs"
# Find RegisterUrl / RegisterInterceptors
grep -r "RegisterUrl\|RegisterInterceptors" --include="*.cs"
# Find proto references
grep -r "GrpcServices" --include="*.csproj"
# Find ValidateOnStart
grep -r "ValidateOnStart" --include="*.cs"
ServicesURLsOptions with [Required, Url]AddGrpcClient<T> call in AddGrpcClients using RegisterUrl helperProtos/ directory<Protobuf> item in .csproj with GrpcServices="Client"appsettings.json under ServicesURLs section