skills/csharp-grpc/SKILL.md
gRPC 服務開發規範:Proto 檔案管理、服務實作、攔截器、錯誤處理與用戶端工廠模式。
npx skillsauth add CloudyWing/ai-dotfiles csharp-grpcInstall 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.
當偵測到專案使用 gRPC(含 Grpc.AspNetCore 或 Google.Protobuf 相依套件)或使用者要求撰寫 gRPC 服務時,請自動套用以下規範。
src/
├── Protos/
│ ├── order_service.proto
│ └── product_service.proto
├── Services/
│ ├── OrderGrpcService.cs
│ └── ProductGrpcService.cs
Protos/ 目錄。order_service.proto),與 Protobuf 社群慣例一致。.proto 檔案對應一個邏輯上的服務domain。syntax = "proto3";
option csharp_namespace = "MyApp.Grpc";
package myapp.v1;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}
message GetOrderRequest {
int32 id = 1;
}
message GetOrderResponse {
int32 id = 1;
string customer_name = 2;
google.protobuf.Timestamp created_at = 3;
repeated OrderItemMessage items = 4;
}
message OrderItemMessage {
int32 product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
csharp_namespace,避免產生的 C# 程式碼落入預設命名空間。package,含版本號(如 myapp.v1)。reserved 保留舊欄位號。myapp.v2)。<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
</ItemGroup>
| 屬性值 | 用途 |
| --- | --- |
| Server | 僅產生伺服器端基底類別 |
| Client | 僅產生用戶端 Stub |
| Both | 同時產生(同專案需要測試或自呼叫時) |
public class OrderGrpcService(
IOrderService orderService,
ILogger<OrderGrpcService> logger
) : OrderService.OrderServiceBase {
public override async Task<GetOrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context
) {
Order? order = await orderService
.FindByIdAsync(request.Id, context.CancellationToken)
.ConfigureAwait(false);
if (order is null) {
throw new RpcException(new Status(StatusCode.NotFound, $"訂單 {request.Id} 不存在"));
}
return new GetOrderResponse {
Id = order.Id,
CustomerName = order.CustomerName,
CreatedAt = Timestamp.FromDateTime(order.CreatedAt.ToUniversalTime())
};
}
public override async Task<ListOrdersResponse> ListOrders(
ListOrdersRequest request,
ServerCallContext context
) {
IReadOnlyList<Order> orders = await orderService
.GetAllAsync(context.CancellationToken)
.ConfigureAwait(false);
ListOrdersResponse response = new();
response.Orders.AddRange(orders.Select(o => new GetOrderResponse {
Id = o.Id,
CustomerName = o.CustomerName
}));
return response;
}
}
GrpcService 結尾,與 Domain Service 區分。ServerCallContext.CancellationToken 傳入所有非同步操作。| gRPC StatusCode | HTTP 對應 | 適用情境 |
| --- | --- | --- |
| OK | 200 | 成功 |
| InvalidArgument | 400 | 參數格式錯誤 |
| NotFound | 404 | 資源不存在 |
| AlreadyExists | 409 | 資源已存在 |
| PermissionDenied | 403 | 無權限 |
| Unauthenticated | 401 | 未認證 |
| FailedPrecondition | 400 | 前置條件不滿足 |
| Internal | 500 | 伺服器內部錯誤 |
| Unavailable | 503 | 服務暫時不可用 |
// ✅ 正確:使用 RpcException 搭配明確的 StatusCode
throw new RpcException(new Status(StatusCode.InvalidArgument, "客戶名稱不可為空"));
// ❌ 錯誤:拋出一般例外(用戶端只會收到 StatusCode.Unknown)
throw new ArgumentException("客戶名稱不可為空");
public class ErrorInterceptor(ILogger<ErrorInterceptor> logger) : Interceptor {
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation
) {
try {
return await continuation(request, context).ConfigureAwait(false);
} catch (RpcException) {
throw; // 已處理的 gRPC 錯誤直接傳播
} catch (Exception ex) {
logger.LogError(ex, "gRPC 方法 {Method} 發生未處理的例外", context.Method);
throw new RpcException(new Status(StatusCode.Internal, "伺服器內部錯誤"));
}
}
}
// 註冊
builder.Services.AddGrpc(options => {
options.Interceptors.Add<ErrorInterceptor>();
options.Interceptors.Add<LoggingInterceptor>();
});
// 註冊
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(options => {
options.Address = new Uri("https://localhost:5001");
})
.ConfigureChannel(options => {
options.MaxRetryAttempts = 3;
});
// 使用
public class OrderProxy(OrderService.OrderServiceClient client) {
public async Task<GetOrderResponse> GetOrderAsync(int id, CancellationToken cancellationToken) {
return await client.GetOrderAsync(
new GetOrderRequest { Id = id },
cancellationToken: cancellationToken
).ConfigureAwait(false);
}
}
GrpcChannel。使用 AddGrpcClient 搭配 HttpClientFactory,獲得連線管理與重試策略。public override async Task StreamOrders(
StreamOrdersRequest request,
IServerStreamWriter<OrderMessage> responseStream,
ServerCallContext context
) {
await foreach (Order order in orderService
.GetOrderStreamAsync(context.CancellationToken)
.ConfigureAwait(false)
) {
await responseStream.WriteAsync(new OrderMessage { Id = order.Id }, context.CancellationToken)
.ConfigureAwait(false);
}
}
context.CancellationToken,用戶端中斷時及時停止。// Program.cs
builder.Services.AddGrpc();
app.MapGrpcService<OrderGrpcService>();
app.MapGrpcService<ProductGrpcService>();
repeated 欄位(集合)避免傳送過大的資料量,超過 4 MB 建議改用 Server Streaming 分批傳送。MaxReceiveMessageSize / MaxSendMessageSize 調整。tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.
data-ai
多份資料檔整合流程。當需要將兩份以上的資料檔(如 JSON、CSV)合併、補齊闕漏欄位或去重成單一檔案時使用。以 dry-run、筆數核對與抽樣比對降低整合錯誤。