| name | dev-grpc-service-designer |
| description | 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". |
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
brew install protobuf
apt install -y protobuf-compiler
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
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
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)
{
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);
stream.WriteAsync( PaymentStatusEvent { PaymentId = req.PaymentId, Status = status });
(status PaymentStatus.Completed PaymentStatus.Failed) ;
Task.Delay(TimeSpan.FromSeconds(), ctx.CancellationToken);
}
}
}
Enregistrement ASP.NET Core
builder.Services.AddGrpc(opt =>
{
opt.MaxReceiveMessageSize = 4 * 1024 * 1024;
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
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.