From dotnet-ai-kit
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dotnet-ai-kit:grpc-designWhen to use
When designing gRPC APIs, proto file conventions, or gRPC-Web configuration
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
- **Contract-first** — The `.proto` file IS the API. Design the proto, then generate code. Never hand-write generated stubs.
.proto file IS the API. Design the proto, then generate code. Never hand-write generated stubs.google.protobuf.Any and string for structured data.Use lowercase_with_underscores.proto. One service per file.
protos/
company/
ordering/
v1/
order_service.proto
order_messages.proto
v2/
order_service.proto
catalog/
v1/
catalog_service.proto
Use reverse domain with version suffix: company.domain.v1.
syntax = "proto3";
package contoso.ordering.v1;
option csharp_namespace = "Contoso.Ordering.V1";
PascalCase for services and RPCs. Use verb-noun for RPC methods.
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
rpc StreamOrderUpdates(StreamOrderUpdatesRequest) returns (stream OrderUpdate);
}
Every RPC gets its own Request and Response message, even if they seem similar. This allows independent evolution.
message CreateOrderRequest {
string customer_id = 1;
repeated OrderLineItem items = 2;
google.protobuf.StringValue coupon_code = 3; // nullable
}
message CreateOrderResponse {
string order_id = 1;
google.protobuf.Timestamp created_at = 2;
}
reserved.message Order {
reserved 6, 8; // previously removed fields
reserved "old_status", "legacy"; // reserved names prevent accidental reuse
string order_id = 1;
string customer_id = 2;
OrderStatus status = 3;
google.protobuf.Timestamp created_at = 4;
repeated OrderLineItem items = 5;
// field 6 was removed
ShippingAddress shipping_address = 7;
// field 8 was removed
DecimalValue total_amount = 9;
}
| .NET Type | Protobuf Type | Import |
|---|---|---|
string? | google.protobuf.StringValue | google/protobuf/wrappers.proto |
int? | google.protobuf.Int32Value | google/protobuf/wrappers.proto |
bool? | google.protobuf.BoolValue | google/protobuf/wrappers.proto |
DateTime | google.protobuf.Timestamp | google/protobuf/timestamp.proto |
TimeSpan | google.protobuf.Duration | google/protobuf/duration.proto |
decimal | Custom DecimalValue | See below |
Dictionary | map<string, T> | Built-in |
Protobuf has no native decimal. Define a shared type:
// shared/decimal_value.proto
message DecimalValue {
int64 units = 1; // whole units
int32 nanos = 2; // nano units (10^-9)
}
Always include an UNSPECIFIED zero value. Use UPPER_SNAKE_CASE.
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
Use cursor-based pagination for list operations:
message ListOrdersRequest {
int32 page_size = 1; // max items per page
string page_token = 2; // opaque cursor from previous response
string filter = 3; // optional filter expression
string order_by = 4; // optional sort
}
message ListOrdersResponse {
repeated Order orders = 1;
string next_page_token = 2; // empty when no more pages
int32 total_count = 3;
}
Expose gRPC services as RESTful JSON endpoints without a separate API layer.
Add the NuGet package:
<PackageReference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" Version="8.0.*" />
Register in Program.cs:
builder.Services.AddGrpc().AddJsonTranscoding();
import "google/api/annotations.proto";
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order) {
option (google.api.http) = {
get: "/v1/orders/{order_id}"
};
}
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {
option (google.api.http) = {
post: "/v1/orders"
body: "*"
};
}
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse) {
option (google.api.http) = {
get: "/v1/orders"
};
}
}
This serves both gRPC (binary/HTTP2) and REST (JSON/HTTP1.1) on the same port.
var app = builder.Build();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.MapGrpcService<OrderServiceImpl>().EnableGrpcWeb();
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://api.contoso.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new GrpcWebHandler(new HttpClientHandler()));
Limitation: gRPC-Web does not support client streaming or bidirectional streaming. Only unary and server streaming work.
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.62.*" PrivateAssets="All" />
<PackageReference Include="Grpc.Net.Client" Version="2.62.*" />
<PackageReference Include="Google.Protobuf" Version="3.26.*" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\**\*.proto"
GrpcServices="Client"
ProtoRoot="Protos" />
</ItemGroup>
For server projects, use GrpcServices="Server". For shared projects with both, use GrpcServices="Both".
Create a dedicated NuGet package for proto files so all services consume the same contract:
Contoso.Ordering.Protos/
Protos/
contoso/ordering/v1/order_service.proto
Contoso.Ordering.Protos.csproj
public sealed class OrderServiceImpl : OrderService.OrderServiceBase
{
private readonly IOrderRepository _repository;
private readonly ILogger<OrderServiceImpl> _logger;
public OrderServiceImpl(IOrderRepository repository, ILogger<OrderServiceImpl> logger)
{
_repository = repository;
_logger = logger;
}
public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
// Validate
if (request.Items.Count == 0)
{
throw new RpcException(new Status(
StatusCode.InvalidArgument, "Order must contain at least one item."));
}
// Map and persist
var order = OrderMapper.ToDomain(request);
await _repository.AddAsync(order, context.CancellationToken);
_logger.LogInformation("Order {OrderId} created for customer {CustomerId}",
order.Id, order.CustomerId);
return new CreateOrderResponse
{
OrderId = order.Id.ToString(),
CreatedAt = Timestamp.FromDateTime(order.CreatedAt)
};
}
public override async Task<Order> GetOrder(
GetOrderRequest request, ServerCallContext context)
{
var order = await _repository.GetByIdAsync(
request.OrderId, context.CancellationToken);
if (order is null)
{
throw new RpcException(new Status(
StatusCode.NotFound, $"Order '{request.OrderId}' not found."));
}
return OrderMapper.ToProto(order);
}
public override async Task StreamOrderUpdates(
StreamOrderUpdatesRequest request,
IServerStreamWriter<OrderUpdate> responseStream,
ServerCallContext context)
{
await foreach (var update in _repository
.WatchOrderAsync(request.OrderId, context.CancellationToken))
{
await responseStream.WriteAsync(OrderMapper.ToUpdateProto(update));
}
}
}
Map domain exceptions to gRPC status codes via interceptor (see skills/microservice/grpc/interceptors/SKILL.md for full pattern):
| gRPC Status Code | Use When |
|---|---|
InvalidArgument | Request validation fails |
NotFound | Entity does not exist |
AlreadyExists | Duplicate create attempt |
PermissionDenied | Caller lacks permission |
FailedPrecondition | Business rule violation |
Internal | Unexpected server error (do not leak details) |
v2), run both, migrate graduallyreserved to mark removed fields so numbers are never reused| Scenario | Recommended | Reason |
|---|---|---|
| Internal microservice-to-microservice | gRPC | Performance, streaming, strong contracts |
| Public API for third-party developers | REST | Universal client support, documentation tooling |
| Flexible client queries on complex graphs | GraphQL | Client controls response shape |
| Real-time browser push (notifications) | SignalR | Native WebSocket, broad browser support |
| Browser client calling backend services | gRPC-Web or REST | gRPC-Web if already gRPC; REST if simpler |
| High-throughput event streaming | gRPC streaming | Efficient bidirectional binary streams |
| Mobile app with spotty connectivity | REST | Simpler retry, caching, offline support |
| Polyglot services (Go, Java, .NET) | gRPC | Single proto generates all client/server code |
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Reusing deleted field numbers | Wire format corruption for old clients | Use reserved for removed fields |
| No version in package name | Cannot make breaking changes safely | Always include v1, v2 in package |
| Fat messages (50+ fields) | Slow serialization, hard to evolve | Split into nested messages |
Using string for everything | Loses type safety, validation moves to runtime | Use proper types and well-known wrappers |
google.protobuf.Any everywhere | Defeats the purpose of a typed contract | Use oneof for polymorphism |
| No request/response wrappers | Cannot add fields without breaking signature | Every RPC gets its own Request/Response |
| Ignoring deadlines | Cascading timeouts across services | Always set and propagate deadlines |
| Returning internal errors verbatim | Leaks stack traces and implementation details | Map to status codes, log internally |
| Synchronous blocking in async RPCs | Thread pool starvation under load | Use async/await throughout |
Creates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.
npx claudepluginhub faysilalshareef/dotnet-ai-kit