Concepteur de Services gRPC
Workflow en étapes
- Qualifier la communication — choisir le bon pattern (voir tableau ci-dessous) avant d'ouvrir un éditeur.
- Concevoir le contrat
.proto — package versionné, messages dédiés par RPC, enums avec valeur 0 UNSPECIFIED.
- Générer le code —
protoc ou plugin SDK ; vérifier que le code généré ne doit jamais être édité manuellement.
- Implémenter serveur puis client — gestion d'erreurs gRPC status codes, deadlines, idempotence.
- Sécuriser et observer — TLS mutuel, intercepteurs logging/métriques, health checks.
- Valider la compatibilité — tester la rétrocompatibilité binaire avant tout déploiement.
Critère de choix du pattern de communication
| Pattern |
Quand l'utiliser |
Exemple concret |
| Unaire |
Requête/réponse atomique, latence faible |
CRUD, auth, paiement ponctuel |
| Server streaming |
Serveur pousse un volume inconnu |
Notifications, export CSV/JSON |
| Client streaming |
Client envoie un flux puis attend le résultat |
Upload fichiers, ingestion de logs |
| Bidirectionnel |
Dialogue continu, ordre important |
Chat temps réel, monitoring interactif |
Règle heuristique : si la réponse tient dans une seule trame TCP et que le serveur répond immédiatement → unaire. Dès qu'une des parties envoie plusieurs messages → streaming.
Conception du contrat Protobuf
Structure de référence
syntax = "proto3";
package myapp.payments.v1;
option csharp_namespace = "MyApp.Payments.V1";
option go_package = "github.com/myapp/payments/v1;paymentsv1";
import "google/protobuf/timestamp.proto";
service PaymentService {
// Unaire
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse);
// Server streaming
rpc WatchStatus(WatchStatusRequest) returns (stream PaymentStatusEvent);
// Client streaming
rpc BatchImport(stream ImportTransactionRequest) returns (BatchImportResponse);
// Bidirectionnel
rpc SyncLedger(stream LedgerEntry) returns (stream LedgerAck);
}
message CreatePaymentRequest {
string idempotency_key = 1; // UUID v4, obligatoire
int64 amount_cents = 2;
string currency = 3; // ISO 4217
string recipient_id = 4;
map<string, string> metadata = 5;
}
message CreatePaymentResponse {
string payment_id = 1;
PaymentStatus status = 2;
google.protobuf.Timestamp created_at = 3;
}
enum PaymentStatus {
PAYMENT_STATUS_UNSPECIFIED = 0; // obligatoire
PAYMENT_STATUS_PENDING = 1;
PAYMENT_STATUS_PROCESSING = 2;
PAYMENT_STATUS_COMPLETED = 3;
PAYMENT_STATUS_FAILED = 4;
}
// Champs supprimés → reserved, jamais effacés
// reserved 6, 7;
// reserved "old_field_name";
Conventions de nommage
| Élément |
Convention |
Exemple |
| Package |
company.service.v1 |
myapp.payments.v1 |
| Service |
PascalCase + Service |
PaymentService |
| RPC |
PascalCase, verbe d'action |
CreatePayment |
| Request/Response |
Propre à chaque RPC |
CreatePaymentRequest |
| Champ |
snake_case |
payment_id |
| Enum |
SCREAMING_SNAKE_CASE avec préfixe enum |
PAYMENT_STATUS_PENDING |
| Enum valeur 0 |
Toujours UNSPECIFIED |
PAYMENT_STATUS_UNSPECIFIED |
Génération du code
# Installer protoc + plugins
brew install protobuf # macOS
apt install -y protobuf-compiler # Debian/Ubuntu
# Go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Générer
protoc --proto_path=proto \
--go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
proto/payments/v1/payment.proto
# C# / .NET : via NuGet Grpc.Tools (MSBuild auto-génère à la build)
# Java : via grpc-java plugin Maven/Gradle
Implémentation C# / ASP.NET Core
Serveur
public sealed class PaymentServiceImpl : PaymentService.PaymentServiceBase
{
private readonly IPaymentRepository _repo;
private readonly ILogger<PaymentServiceImpl> _log;
public override async Task<CreatePaymentResponse> CreatePayment(
CreatePaymentRequest req, ServerCallContext ctx)
{
// Idempotence
var existing = await _repo.FindByIdempotencyKeyAsync(req.IdempotencyKey, ctx.CancellationToken);
if (existing is not null) return Map(existing);
if (req.AmountCents <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "amount_cents must be > 0"));
var payment = await _repo.CreateAsync(req.AmountCents, req.Currency, req.RecipientId, ctx.CancellationToken);
_log.LogInformation("Payment {Id} created", payment.Id);
return Map(payment);
}
public override async Task WatchStatus(
WatchStatusRequest req,
IServerStreamWriter<PaymentStatusEvent> stream,
ServerCallContext ctx)
{
while (!ctx.CancellationToken.IsCancellationRequested)
{
var status = await _repo.GetStatusAsync(req.PaymentId, ctx.CancellationToken);
await stream.WriteAsync(new PaymentStatusEvent { PaymentId = req.PaymentId, Status = status });
if (status is PaymentStatus.Completed or PaymentStatus.Failed) break;
await Task.Delay(TimeSpan.FromSeconds(1), ctx.CancellationToken);
}
}
}
Enregistrement ASP.NET Core
// Program.cs
builder.Services.AddGrpc(opt =>
{
opt.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
opt.EnableDetailedErrors = builder.Environment.IsDevelopment();
opt.Interceptors.Add<ServerLoggingInterceptor>();
opt.Interceptors.Add<ServerMetricsInterceptor>();
});
builder.Services.AddGrpcHealthChecks();
app.MapGrpcService<PaymentServiceImpl>();
app.MapGrpcHealthChecksService();
Client avec deadline et retry
var channel = GrpcChannel.ForAddress("https://payments.internal:5001", new GrpcChannelOptions
{
ServiceConfig = new ServiceConfig
{
MethodConfigs = { new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromMilliseconds(100),
MaxBackoff = TimeSpan.FromSeconds(2),
BackoffMultiplier = 2,
RetryableStatusCodes = { StatusCode.Unavailable }
}
}}
}
});
var client = new PaymentService.PaymentServiceClient(channel);
var reply = await client.CreatePaymentAsync(
new CreatePaymentRequest { IdempotencyKey = Guid.NewGuid().ToString(), AmountCents = 1500, Currency = "TND" },
deadline: DateTime.UtcNow.AddSeconds(5));
Gestion des erreurs — Status codes à utiliser
| Situation |
Status code gRPC |
| Champ manquant / invalide |
INVALID_ARGUMENT |
| Ressource introuvable |
NOT_FOUND |
| Conflit (doublon) |
ALREADY_EXISTS |
| Non authentifié |
UNAUTHENTICATED |
| Accès refusé |
PERMISSION_DENIED |
| Timeout / deadline dépassée |
DEADLINE_EXCEEDED |
| Service indisponible |
UNAVAILABLE |
| Erreur interne |
INTERNAL |
Toujours lever RpcException côté serveur — ne jamais laisser remonter une exception .NET brute.
Versionning et compatibilité
- Rétrocompatible : ajouter de nouveaux champs (numéros supérieurs), nouvelles valeurs d'enum.
- Breaking change : changer le type d'un champ, renommer, supprimer → nouvelle version (
v2).
- Champs supprimés :
reserved 5; reserved "old_name"; — jamais effacés.
- Déployer les deux versions en parallèle pendant la période de migration.
Garde-fous et anti-patterns
| Anti-pattern |
Problème |
Correction |
Réutiliser Request entre plusieurs RPCs |
Couplage fort, évolution impossible |
Un Request/Response par RPC |
Champ string pour les montants monétaires |
Arrondi, parsing |
int64 amount_cents |
| Pas de deadline côté client |
Appels pendants indéfinis |
Toujours passer deadline: |
| Écrire dans le code généré |
Perdu à la prochaine génération |
Ne toucher qu'aux fichiers .proto |
| Enum sans valeur 0 |
Decode incohérent protobuf3 |
Toujours FOO_UNSPECIFIED = 0 |
Message google.protobuf.Empty en réponse |
Pas d'évolution possible |
Toujours un message dédié XxxResponse |
| Streaming pour des requêtes unitaires simples |
Complexité inutile |
Unaire si un message suffit |
| TLS désactivé en prod |
Données en clair |
mTLS obligatoire hors cluster privé |
Checklist avant livraison
1---2name: grpc-service-designer3description: Conception de services gRPC, définition de contrats Protobuf, patterns de streaming et intégration dans des architectures microservices. À utiliser quand l'utilisateur conçoit des APIs gRPC, écrit des fichiers .proto ou implémente du streaming. Se déclenche aussi avec "gRPC", "protobuf", "fichier proto", "streaming gRPC", "service gRPC", "contrat protobuf". Also triggers on "gRPC service", "protobuf contract", "bidirectional streaming".4---56# Concepteur de Services gRPC78## Workflow en étapes9101. **Qualifier la communication** — choisir le bon pattern (voir tableau ci-dessous) avant d'ouvrir un éditeur.112. **Concevoir le contrat `.proto`** — package versionné, messages dédiés par RPC, enums avec valeur 0 `UNSPECIFIED`.123. **Générer le code** — `protoc` ou plugin SDK ; vérifier que le code généré ne doit jamais être édité manuellement.134. **Implémenter serveur puis client** — gestion d'erreurs gRPC status codes, deadlines, idempotence.145. **Sécuriser et observer** — TLS mutuel, intercepteurs logging/métriques, health checks.156. **Valider la compatibilité** — tester la rétrocompatibilité binaire avant tout déploiement.1617## Critère de choix du pattern de communication1819| Pattern | Quand l'utiliser | Exemple concret |20|---------|-----------------|-----------------|21| **Unaire** | Requête/réponse atomique, latence faible | CRUD, auth, paiement ponctuel |22| **Server streaming** | Serveur pousse un volume inconnu | Notifications, export CSV/JSON |23| **Client streaming** | Client envoie un flux puis attend le résultat | Upload fichiers, ingestion de logs |24| **Bidirectionnel** | Dialogue continu, ordre important | Chat temps réel, monitoring interactif |2526Règle heuristique : si la réponse tient dans une seule trame TCP et que le serveur répond immédiatement → unaire. Dès qu'une des parties envoie plusieurs messages → streaming.2728## Conception du contrat Protobuf2930### Structure de référence3132```protobuf33syntax = "proto3";3435package myapp.payments.v1;3637option csharp_namespace = "MyApp.Payments.V1";38option go_package = "github.com/myapp/payments/v1;paymentsv1";3940import "google/protobuf/timestamp.proto";4142service PaymentService {43 // Unaire44 rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse);45 // Server streaming46 rpc WatchStatus(WatchStatusRequest) returns (stream PaymentStatusEvent);47 // Client streaming48 rpc BatchImport(stream ImportTransactionRequest) returns (BatchImportResponse);49 // Bidirectionnel50 rpc SyncLedger(stream LedgerEntry) returns (stream LedgerAck);51}5253message CreatePaymentRequest {54 string idempotency_key = 1; // UUID v4, obligatoire55 int64 amount_cents = 2;56 string currency = 3; // ISO 421757 string recipient_id = 4;58 map<string, string> metadata = 5;59}6061message CreatePaymentResponse {62 string payment_id = 1;63 PaymentStatus status = 2;64 google.protobuf.Timestamp created_at = 3;65}6667enum PaymentStatus {68 PAYMENT_STATUS_UNSPECIFIED = 0; // obligatoire69 PAYMENT_STATUS_PENDING = 1;70 PAYMENT_STATUS_PROCESSING = 2;71 PAYMENT_STATUS_COMPLETED = 3;72 PAYMENT_STATUS_FAILED = 4;73}7475// Champs supprimés → reserved, jamais effacés76// reserved 6, 7;77// reserved "old_field_name";78```7980### Conventions de nommage8182| Élément | Convention | Exemple |83|---------|-----------|---------|84| Package | `company.service.v1` | `myapp.payments.v1` |85| Service | PascalCase + `Service` | `PaymentService` |86| RPC | PascalCase, verbe d'action | `CreatePayment` |87| Request/Response | Propre à chaque RPC | `CreatePaymentRequest` |88| Champ | snake_case | `payment_id` |89| Enum | SCREAMING_SNAKE_CASE avec préfixe enum | `PAYMENT_STATUS_PENDING` |90| Enum valeur 0 | Toujours `UNSPECIFIED` | `PAYMENT_STATUS_UNSPECIFIED` |9192### Génération du code9394```bash95# Installer protoc + plugins96brew install protobuf # macOS97apt install -y protobuf-compiler # Debian/Ubuntu9899# Go100go install google.golang.org/protobuf/cmd/protoc-gen-go@latest101go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest102103# Générer104protoc --proto_path=proto \105 --go_out=gen --go_opt=paths=source_relative \106 --go-grpc_out=gen --go-grpc_opt=paths=source_relative \107 proto/payments/v1/payment.proto108109# C# / .NET : via NuGet Grpc.Tools (MSBuild auto-génère à la build)110# Java : via grpc-java plugin Maven/Gradle111```112113## Implémentation C# / ASP.NET Core114115### Serveur116117```csharp118public sealed class PaymentServiceImpl : PaymentService.PaymentServiceBase119{120 private readonly IPaymentRepository _repo;121 private readonly ILogger<PaymentServiceImpl> _log;122123 public override async Task<CreatePaymentResponse> CreatePayment(124 CreatePaymentRequest req, ServerCallContext ctx)125 {126 // Idempotence127 var existing = await _repo.FindByIdempotencyKeyAsync(req.IdempotencyKey, ctx.CancellationToken);128 if (existing is not null) return Map(existing);129130 if (req.AmountCents <= 0)131 throw new RpcException(new Status(StatusCode.InvalidArgument, "amount_cents must be > 0"));132133 var payment = await _repo.CreateAsync(req.AmountCents, req.Currency, req.RecipientId, ctx.CancellationToken);134 _log.LogInformation("Payment {Id} created", payment.Id);135 return Map(payment);136 }137138 public override async Task WatchStatus(139 WatchStatusRequest req,140 IServerStreamWriter<PaymentStatusEvent> stream,141 ServerCallContext ctx)142 {143 while (!ctx.CancellationToken.IsCancellationRequested)144 {145 var status = await _repo.GetStatusAsync(req.PaymentId, ctx.CancellationToken);146 await stream.WriteAsync(new PaymentStatusEvent { PaymentId = req.PaymentId, Status = status });147148 if (status is PaymentStatus.Completed or PaymentStatus.Failed) break;149150 await Task.Delay(TimeSpan.FromSeconds(1), ctx.CancellationToken);151 }152 }153}154```155156### Enregistrement ASP.NET Core157158```csharp159// Program.cs160builder.Services.AddGrpc(opt =>161{162 opt.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB163 opt.EnableDetailedErrors = builder.Environment.IsDevelopment();164 opt.Interceptors.Add<ServerLoggingInterceptor>();165 opt.Interceptors.Add<ServerMetricsInterceptor>();166});167builder.Services.AddGrpcHealthChecks();168169app.MapGrpcService<PaymentServiceImpl>();170app.MapGrpcHealthChecksService();171```172173### Client avec deadline et retry174175```csharp176var channel = GrpcChannel.ForAddress("https://payments.internal:5001", new GrpcChannelOptions177{178 ServiceConfig = new ServiceConfig179 {180 MethodConfigs = { new MethodConfig181 {182 Names = { MethodName.Default },183 RetryPolicy = new RetryPolicy184 {185 MaxAttempts = 3,186 InitialBackoff = TimeSpan.FromMilliseconds(100),187 MaxBackoff = TimeSpan.FromSeconds(2),188 BackoffMultiplier = 2,189 RetryableStatusCodes = { StatusCode.Unavailable }190 }191 }}192 }193});194195var client = new PaymentService.PaymentServiceClient(channel);196197var reply = await client.CreatePaymentAsync(198 new CreatePaymentRequest { IdempotencyKey = Guid.NewGuid().ToString(), AmountCents = 1500, Currency = "TND" },199 deadline: DateTime.UtcNow.AddSeconds(5));200```201202## Gestion des erreurs — Status codes à utiliser203204| Situation | Status code gRPC |205|-----------|-----------------|206| Champ manquant / invalide | `INVALID_ARGUMENT` |207| Ressource introuvable | `NOT_FOUND` |208| Conflit (doublon) | `ALREADY_EXISTS` |209| Non authentifié | `UNAUTHENTICATED` |210| Accès refusé | `PERMISSION_DENIED` |211| Timeout / deadline dépassée | `DEADLINE_EXCEEDED` |212| Service indisponible | `UNAVAILABLE` |213| Erreur interne | `INTERNAL` |214215Toujours lever `RpcException` côté serveur — ne jamais laisser remonter une exception .NET brute.216217## Versionning et compatibilité218219- **Rétrocompatible** : ajouter de nouveaux champs (numéros supérieurs), nouvelles valeurs d'enum.220- **Breaking change** : changer le type d'un champ, renommer, supprimer → nouvelle version (`v2`).221- Champs supprimés : `reserved 5; reserved "old_name";` — jamais effacés.222- Déployer les deux versions en parallèle pendant la période de migration.223224## Garde-fous et anti-patterns225226| Anti-pattern | Problème | Correction |227|---|---|---|228| Réutiliser `Request` entre plusieurs RPCs | Couplage fort, évolution impossible | Un `Request`/`Response` par RPC |229| Champ `string` pour les montants monétaires | Arrondi, parsing | `int64 amount_cents` |230| Pas de deadline côté client | Appels pendants indéfinis | Toujours passer `deadline:` |231| Écrire dans le code généré | Perdu à la prochaine génération | Ne toucher qu'aux fichiers `.proto` |232| Enum sans valeur 0 | Decode incohérent protobuf3 | Toujours `FOO_UNSPECIFIED = 0` |233| Message `google.protobuf.Empty` en réponse | Pas d'évolution possible | Toujours un message dédié `XxxResponse` |234| Streaming pour des requêtes unitaires simples | Complexité inutile | Unaire si un message suffit |235| TLS désactivé en prod | Données en clair | mTLS obligatoire hors cluster privé |236237## Checklist avant livraison238239- [ ] `.proto` dans un package versionné (`v1`)240- [ ] Chaque RPC a son propre `Request` et `Response`241- [ ] Enum valeur 0 = `UNSPECIFIED`242- [ ] Champs supprimés marqués `reserved`243- [ ] Deadlines configurées côté client244- [ ] Intercepteurs logging + métriques côté serveur245- [ ] Health check gRPC exposé246- [ ] Tests de compatibilité binaire (ex : `buf breaking`)