agentsclimarketplace

Grpc design

Skill FaysilAlshareef/dotnet-ai-kit/skills/api/grpc-design

Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.From its SKILL.md

Install
npx -y skills add FaysilAlshareef/dotnet-ai-kit --skill grpc-design

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.4 KB, as published. Nobody here has run it

gRPC API Design for .NET

1. Core Principles

  • Contract-first — The .proto file IS the API. Design the proto, then generate code. Never hand-write generated stubs.
  • Backward compatible — Every change must be safe for existing clients. Never break the wire format.
  • Strongly typed — Leverage protobuf's type system. Avoid google.protobuf.Any and string for structured data.
  • Service-oriented — Each service represents a bounded context or aggregate boundary. Keep services cohesive.
  • Efficient by default — Binary serialization, HTTP/2 multiplexing, and streaming are built in. Use them intentionally.

2. When to Use gRPC

Use gRPC When

  • Service-to-service communication in microservices
  • High-throughput, low-latency requirements
  • Bidirectional or server streaming is needed
  • Polyglot environments (shared proto contracts across .NET, Go, Java, Python)
  • Strong contract enforcement is critical
  • Internal APIs behind a gateway

Do NOT Use gRPC When

  • Browser clients need direct access (use REST or gRPC-Web with a proxy)
  • You need human-readable payloads for debugging (use REST + JSON)
  • The API is public-facing and must be universally consumable
  • Simple CRUD with no streaming or performance needs (REST is simpler)
  • Real-time push to many browser clients (use SignalR instead)
  • The team has no protobuf experience and the timeline is tight

3. Proto File Conventions

File Naming

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

Package Naming

Use reverse domain with version suffix: company.domain.v1.

syntax = "proto3";

package contoso.ordering.v1;

option csharp_namespace = "Contoso.Ordering.V1";

Service Naming

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);
}

Request/Response Pattern

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;
}

4. Message Design

Field Numbering Rules

  • Never reuse a field number after removing a field. Use reserved.
  • Fields 1-15 use 1 byte for the tag — reserve these for frequently set fields.
  • Fields 16-2047 use 2 bytes.
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;
}

Well-Known Type Mappings

.NET TypeProtobuf TypeImport
string?google.protobuf.StringValuegoogle/protobuf/wrappers.proto
int?google.protobuf.Int32Valuegoogle/protobuf/wrappers.proto
bool?google.protobuf.BoolValuegoogle/protobuf/wrappers.proto
DateTimegoogle.protobuf.Timestampgoogle/protobuf/timestamp.proto
TimeSpangoogle.protobuf.Durationgoogle/protobuf/duration.proto
decimalCustom DecimalValueSee below
Dictionarymap<string, T>Built-in

Custom DecimalValue for Precision

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)
}

Enums

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;
}

Pagination

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;
}

5. gRPC-JSON Transcoding

Expose gRPC services as RESTful JSON endpoints without a separate API layer.

Setup

Add the NuGet package:

<PackageReference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" Version="8.0.*" />

Register in Program.cs:

builder.Services.AddGrpc().AddJsonTranscoding();

Proto Annotations

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.

6. gRPC-Web for Blazor and Browser Clients

Server Configuration

var app = builder.Build();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.MapGrpcService<OrderServiceImpl>().EnableGrpcWeb();

Blazor Client Setup

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.

7. Client Generation with Grpc.Tools

Project File Configuration

<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".

Shared Proto Package

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

8. C# Service Implementation

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));
        }
    }
}

Error Handling

Map domain exceptions to gRPC status codes via interceptor (see skills/microservice/grpc/interceptors/SKILL.md for full pattern):

gRPC Status CodeUse When
InvalidArgumentRequest validation fails
NotFoundEntity does not exist
AlreadyExistsDuplicate create attempt
PermissionDeniedCaller lacks permission
FailedPreconditionBusiness rule violation
InternalUnexpected server error (do not leak details)

9. Proto Versioning Strategy

  • Non-breaking (add fields/RPCs/enum values): do in-place
  • Breaking (rename/remove/change types): create new version (v2), run both, migrate gradually
  • Use reserved to mark removed fields so numbers are never reused

10. Decision Guide

ScenarioRecommendedReason
Internal microservice-to-microservicegRPCPerformance, streaming, strong contracts
Public API for third-party developersRESTUniversal client support, documentation tooling
Flexible client queries on complex graphsGraphQLClient controls response shape
Real-time browser push (notifications)SignalRNative WebSocket, broad browser support
Browser client calling backend servicesgRPC-Web or RESTgRPC-Web if already gRPC; REST if simpler
High-throughput event streaminggRPC streamingEfficient bidirectional binary streams
Mobile app with spotty connectivityRESTSimpler retry, caching, offline support
Polyglot services (Go, Java, .NET)gRPCSingle proto generates all client/server code

11. Anti-Patterns

Anti-PatternProblemFix
Reusing deleted field numbersWire format corruption for old clientsUse reserved for removed fields
No version in package nameCannot make breaking changes safelyAlways include v1, v2 in package
Fat messages (50+ fields)Slow serialization, hard to evolveSplit into nested messages
Using string for everythingLoses type safety, validation moves to runtimeUse proper types and well-known wrappers
google.protobuf.Any everywhereDefeats the purpose of a typed contractUse oneof for polymorphism
No request/response wrappersCannot add fields without breaking signatureEvery RPC gets its own Request/Response
Ignoring deadlinesCascading timeouts across servicesAlways set and propagate deadlines
Returning internal errors verbatimLeaks stack traces and implementation detailsMap to status codes, log internally
Synchronous blocking in async RPCsThread pool starvation under loadUse async/await throughout

Related Knowledge

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.