| name | service-definition |
| description | Use when designing proto files or implementing gRPC service classes with MediatR. |
| metadata | {"category":"microservice/grpc","agent":"command-architect"} |
| when_to_use | When creating or modifying gRPC service definitions or protobuf files |
Service Definition — Proto Files & Implementation
Core Principles
- Proto files define the service contract (
.proto files)
- Service classes inherit from generated
*Base class
- MediatR dispatches from gRPC service to handlers
- Mapping extensions convert between proto and domain types
- Proto types:
google.protobuf.StringValue for nullable, google.protobuf.Timestamp for DateTime
- Pagination uses
PageRequest/PageResponse messages
Key Patterns
Proto File Structure
syntax = "proto3";
package {company}.{domain};
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "{Company}.{Domain}.Grpc";
// Command service
service OrderCommands {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc UpdateOrder (UpdateOrderRequest) returns (UpdateOrderResponse);
rpc CompleteOrder (CompleteOrderRequest) returns (CompleteOrderResponse);
}
// Query service
service OrderQueries {
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc GetOrders (GetOrdersRequest) returns (GetOrdersResponse);
}
// Messages
message CreateOrderRequest {
string customer_name = 1;
// C-Q3 fix: use minor-unit integers (cents) for money — `double` loses
// precision and is the canonical money anti-pattern. See the anti-pattern
// table below for the recommended choices (int64 cents, string-decimal,
// or google.type.Money / DecimalValue wrapper).
int64 total_cents = 2;
repeated OrderItemMessage items = 3;
}
message CreateOrderResponse {
string order_id = 1;
int32 sequence = 2;
}
message GetOrdersRequest {
int32 page = 1;
int32 page_size = 2;
google.protobuf.StringValue search = 3; // nullable
google.protobuf.StringValue status = 4; // nullable
}
message GetOrdersResponse {
repeated OrderSummary items = 1;
int32 total_count = 2;
int32 page = 3;
int32 page_size = 4;
}
message OrderSummary {
string id = 1;
string customer_name = 2;
int64 total_cents = 3; // C-Q3: minor-unit integer (cents)
string status = 4;
}
message OrderItemMessage {
string product_id = 1;
int32 quantity = 2;
int64 unit_price_cents = 3; // C-Q3: minor-unit integer (cents)
}
Service Implementation
namespace {Company}.{Domain}.Grpc.Services;
public sealed class OrderCommandsService(IMediator mediator)
: OrderCommands.OrderCommandsBase
{
public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
var command = request.ToCommand();
var output = await mediator.Send(command, context.CancellationToken);
return output.ToCreateResponse();
}
public override async Task<UpdateOrderResponse> UpdateOrder(
UpdateOrderRequest request, ServerCallContext context)
{
var command = request.ToCommand();
var output = await mediator.Send(command, context.CancellationToken);
return output.ToUpdateResponse();
}
}
Mapping Extensions
namespace {Company}.{Domain}.Grpc.Extensions;
public static class OrderMappingExtensions
{
public static CreateOrderCommand ToCommand(this CreateOrderRequest r) =>
new(r.CustomerName, (decimal)r.Total,
r.Items.Select(i => new OrderItemInput(
Guid.Parse(i.ProductId), i.Quantity, (decimal)i.UnitPrice
)).ToList());
public static CreateOrderResponse ToCreateResponse(this OrderOutput o) =>
new() { OrderId = o.Id.ToString(), Sequence = o.Sequence };
public static string? ToNullable(this StringValue? value) =>
value?.Value;
public static Timestamp ToTimestamp(this DateTime dt) =>
Timestamp.FromDateTime(dt.ToUniversalTime());
}
Registration in Program.cs
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<ApplicationExceptionInterceptor>();
options.Interceptors.Add<ThreadCultureInterceptor>();
});
var app = builder.Build();
app.MapGrpcService<OrderCommandsService>();
app.MapGrpcService<OrderQueriesService>();
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|
Using string for nullable fields | Use google.protobuf.StringValue |
| Business logic in gRPC service | Delegate to MediatR handlers |
| Missing mapping extensions | Always have explicit ToCommand/ToResponse |
Using float or double for money | C-Q3: use minor-unit integers (int64 cents), string for fixed-point decimals, or a DecimalValue/google.type.Money wrapper. Never double. |
| Hardcoded strings in service | Use resource strings for error messages |
Detect Existing Patterns
find . -name "*.proto" -type f
grep -r "CommandsBase\|QueriesBase" --include="*.cs" src/
grep -r "ToCommand\|ToResponse" --include="*.cs" src/Grpc/
grep -r "MapGrpcService" --include="*.cs" src/
Adding to Existing Project
- Follow existing proto file naming —
{domain}-commands.proto, {domain}-queries.proto
- Match message naming conventions — PascalCase messages, snake_case fields
- Add mapping extensions in
Grpc/Extensions/ directory
- Register new service with
MapGrpcService<T>() in Program.cs
- Update
.csproj with proto file reference (GrpcServices="Server" or "Both")
Related Knowledge