Développement Web

gRPC avec ASP.NET Core 10 : services, streaming et clients typés

10 min de lecture

gRPC est devenu en 2026 le protocole privilégié pour les communications inter-microservices à haute performance dans l’écosystème .NET. ASP.NET Core 10 fournit un support de première classe via le paquet Grpc.AspNetCore, basé sur Protobuf et HTTP/2 (HTTP/3 en preview). Comparé à une API REST JSON équivalente, gRPC offre 5-10× moins de bande passante, des sérialisations 10× plus rapides, et un typage strict bilatéral généré côté client comme serveur. Pour les services internes qui s’échangent des millions de messages par jour, c’est l’option par défaut. Ce tutoriel reprend la mise en place complète : projet, contrat .proto, serveur, client, streaming, authentification, intercepteurs et déploiement.

Prérequis

  • .NET 10 LTS (cf. Installer .NET)
  • Notions ASP.NET Core et C# moderne (cf. C# 14)
  • Outil grpcurl ou grpcui pour les tests
  • Temps estimé : 90 minutes

Étape 1 — Créer un projet gRPC

Le template officiel grpc bootstrappe une API gRPC avec un service exemple Greeter, le fichier proto, et la configuration ASP.NET Core nécessaire. C’est le point de départ standard.

dotnet new grpc -n CataloguService.Grpc
cd CataloguService.Grpc

# Structure générée
# - Program.cs (avec MapGrpcService)
# - Protos/greet.proto (contrat)
# - Services/GreeterService.cs (implémentation)
# - appsettings.json

Le fichier .csproj référence automatiquement Grpc.AspNetCore et configure la génération de code Protobuf à la compilation via Protobuf Include="Protos/*.proto" GrpcServices="Server". Les classes C# correspondant aux messages et services sont générées dans obj/ lors du build.

Étape 2 — Définir le contrat .proto

Le fichier .proto est le contrat qui décrit messages et services en Protobuf 3. Il est partagé entre serveur et clients — généralement publié dans un package séparé ou un repo Git pour synchroniser les équipes. Voici un contrat catalogue type.

syntax = "proto3";

option csharp_namespace = "CatalogueService.Grpc";

package catalogue;

service Catalogue {
  rpc ObtenirProduit (ProduitRequete) returns (ProduitReponse);
  rpc ListerProduits (ListerRequete) returns (stream ProduitReponse);
  rpc CreerProduits (stream CreerProduitRequete) returns (CreerProduitsReponse);
  rpc Discuter (stream MessageClient) returns (stream MessageServeur);
}

message ProduitRequete { string id = 1; }

message ProduitReponse {
  string id = 1;
  string nom = 2;
  double prix_unitaire = 3;
  int32 stock = 4;
  repeated string tags = 5;
}

message ListerRequete {
  int32 page = 1;
  int32 par_page = 2;
  string filtre = 3;
}

message CreerProduitRequete {
  string nom = 1;
  double prix_unitaire = 2;
}

message CreerProduitsReponse {
  int32 total_crees = 1;
  repeated string ids = 2;
}

Quatre patterns de méthodes RPC à connaître. Unary (ObtenirProduit) : requête simple, réponse simple — équivalent REST classique. Server streaming (ListerProduits) : le serveur renvoie un flux — utile pour la pagination, l’export, les logs en temps réel. Client streaming (CreerProduits) : le client envoie un flux — utile pour les uploads par chunks. Bidirectional streaming (Discuter) : flux dans les deux sens — utile pour le chat, la collaboration temps réel.

Étape 3 — Implémenter le service côté serveur

Côté C#, on hérite de la classe abstraite générée Catalogue.CatalogueBase et on implémente chaque RPC. Les Task<T> et les IAsyncEnumerable<T> rendent le streaming naturel.

public class CatalogueService(ILogger<CatalogueService> logger, IProduitsRepository repo)
    : Catalogue.CatalogueBase
{
    public override async Task<ProduitReponse> ObtenirProduit(
        ProduitRequete request, ServerCallContext context)
    {
        var produit = await repo.ObtenirAsync(request.Id, context.CancellationToken);
        if (produit is null)
            throw new RpcException(new Status(StatusCode.NotFound, "Produit introuvable"));

        return new ProduitReponse
        {
            Id = produit.Id,
            Nom = produit.Nom,
            PrixUnitaire = (double)produit.PrixUnitaire,
            Stock = produit.Stock
        };
    }

    public override async Task ListerProduits(
        ListerRequete request,
        IServerStreamWriter<ProduitReponse> responseStream,
        ServerCallContext context)
    {
        await foreach (var p in repo.ListerAsync(request.Page, request.ParPage, context.CancellationToken))
        {
            await responseStream.WriteAsync(new ProduitReponse
            {
                Id = p.Id, Nom = p.Nom, PrixUnitaire = (double)p.PrixUnitaire
            }, context.CancellationToken);
        }
    }
}

Le ServerCallContext donne accès au CancellationToken, aux métadonnées HTTP/2 (équivalents des headers), à l’authentification, et au deadline configuré par le client. Toujours passer le token aux opérations await : si le client raccroche, le serveur arrête immédiatement la requête au lieu de gaspiller des cycles. Les exceptions RpcException portent un StatusCode standard gRPC (NotFound, InvalidArgument, Unauthenticated, etc.) qui se traduit côté client.

Étape 4 — Enregistrer le service dans Program.cs

L’enregistrement gRPC dans ASP.NET Core 10 est minimaliste. On ajoute le service à la DI, on configure les options, et on mappe la route.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 16 * 1024 * 1024;  // 16 Mo
    options.Interceptors.Add<LoggingInterceptor>();
});

builder.Services.AddScoped<IProduitsRepository, ProduitsRepository>();

// gRPC reflection pour grpcurl/grpcui en dev
if (builder.Environment.IsDevelopment())
    builder.Services.AddGrpcReflection();

var app = builder.Build();

app.MapGrpcService<CatalogueService>();
if (app.Environment.IsDevelopment())
    app.MapGrpcReflectionService();

app.Run();

Le EnableDetailedErrors = true en dev affiche la stack trace côté client (utile pour déboguer). En production, on le laisse à false pour ne pas exposer les détails internes. Le MaxReceiveMessageSize à 16 Mo couvre les payloads typiques mais reste sous le seuil 100 Mo qui dégrade la latence. La gRPC reflection permet aux outils comme grpcurl de découvrir les services sans avoir le .proto — bien pratique en développement, à désactiver en prod pour ne pas exposer le schéma.

Étape 5 — Client gRPC : génération et appels

Côté client, le même fichier .proto est référencé avec GrpcServices="Client". Le code généré expose un client typé qu’on injecte dans la DI et qu’on appelle comme une classe normale.

// .csproj du client
<ItemGroup>
  <Protobuf Include="Protos/catalogue.proto" GrpcServices="Client" />
  <PackageReference Include="Grpc.Net.Client" Version="2.80.0" />
  <PackageReference Include="Grpc.Tools" Version="2.80.0" PrivateAssets="All" />
</ItemGroup>

// Program.cs du client
builder.Services.AddGrpcClient<Catalogue.CatalogueClient>(o =>
{
    o.Address = new Uri("https://catalogue.internal:5001");
})
.ConfigureChannel(opt =>
{
    opt.HttpHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
        KeepAlivePingDelay = TimeSpan.FromSeconds(60),
        KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
    };
});

// Usage
public class MonController(Catalogue.CatalogueClient client)
{
    public async Task<ProduitReponse> GetProduit(string id, CancellationToken ct)
    {
        try
        {
            return await client.ObtenirProduitAsync(
                new ProduitRequete { Id = id },
                deadline: DateTime.UtcNow.AddSeconds(5),
                cancellationToken: ct);
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
        {
            return null!;
        }
    }
}

Trois disciplines clés. Le deadline est l’équivalent d’un timeout côté client — il est propagé au serveur via les métadonnées, donc le serveur sait combien de temps il a pour répondre. Le EnableMultipleHttp2Connections évite le bottleneck d’une seule connexion HTTP/2 qui sérialise les requêtes au-delà de 100 simultanées. Le KeepAlive garde la connexion ouverte au-delà des coupures NAT (5 minutes typiques en cloud).

Étape 6 — Intercepteurs (équivalent middleware)

Les intercepteurs gRPC sont l’équivalent des middlewares HTTP : ils s’exécutent avant/après chaque appel pour ajouter logging, authentification, métriques, retry. On les enregistre côté serveur via options.Interceptors ou côté client via AddInterceptor.

public class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            var response = await continuation(request, context);
            logger.LogInformation("RPC {Method} OK {Ms}ms", context.Method, sw.ElapsedMilliseconds);
            return response;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "RPC {Method} échec {Ms}ms", context.Method, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

Pour des intercepteurs plus complexes (streaming), on override aussi ServerStreamingServerHandler, ClientStreamingServerHandler, DuplexStreamingServerHandler. Côté client, les méthodes équivalentes interceptent BlockingUnaryCall et AsyncUnaryCall. C’est ainsi qu’on injecte un token JWT, qu’on retry sur erreur transitoire (Unavailable, DeadlineExceeded), ou qu’on instrumente OpenTelemetry.

Étape 7 — Authentification JWT

Sécuriser un service gRPC suit le même pattern qu’une API REST : JwtBearer middleware ASP.NET Core + [Authorize] sur les services. Le client envoie le token dans les métadonnées authorization.

// Serveur — Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://identity.example.com";
        o.TokenValidationParameters = new() { ValidateAudience = false };
    });
builder.Services.AddAuthorization();

// Service annoté
[Authorize]
public class CatalogueService : Catalogue.CatalogueBase { /* ... */ }

// Client — ajouter un intercepteur qui injecte le token
public class AuthInterceptor(ITokenProvider tokens) : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request, ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var token = tokens.Lire();
        var meta = new Metadata { { "authorization", $"Bearer {token}" } };
        var ctx = context.WithOptions(context.Options.WithHeaders(meta));
        return continuation(request, ctx);
    }
}

Pour les services mTLS internes (zero-trust), le pattern alternatif consiste à exiger un certificat client validé par AddCertificate(). Les deux approches coexistent : JWT pour les utilisateurs/applis, mTLS pour le service-to-service à fort enjeu.

Étape 8 — gRPC-Web pour navigateurs

Les navigateurs ne peuvent pas parler gRPC natif (limitation HTTP/2). gRPC-Web est un protocole proxy qui rend gRPC accessible depuis JavaScript/TypeScript. Le serveur ASP.NET Core supporte gRPC-Web nativement via Grpc.AspNetCore.Web.

// Serveur
builder.Services.AddGrpc();
builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
    p.AllowAnyOrigin().AllowAnyHeader()
     .WithExposedHeaders("grpc-status", "grpc-message", "grpc-encoding", "grpc-accept-encoding")
));

var app = builder.Build();
app.UseGrpcWeb();
app.UseCors();

app.MapGrpcService<CatalogueService>().EnableGrpcWeb().RequireCors();

// Côté client TypeScript avec grpc-web
import { CatalogueClient } from "./generated/catalogue_grpc_web_pb";
const client = new CatalogueClient("https://api.example.com");
client.obtenirProduit({ id: "abc" }, {}, (err, response) => { /* ... */ });

L’overhead de gRPC-Web par rapport au gRPC natif est de 10-20 % en taille (encodage base64 sur la couche transport), mais cela reste sensiblement plus efficace qu’une API REST JSON. Pour les SPA/PWA qui consomment des microservices typés, c’est une excellente alternative à REST.

Erreurs fréquentes

Symptôme Cause Solution
Erreur « HTTP/2 over TLS » Kestrel sans certificat HTTPS Activer ASP.NET Core developer cert : dotnet dev-certs https --trust
StatusCode.Unimplemented côté client Service non mappé ou nom de méthode incorrect Vérifier MapGrpcService et la régénération du .proto
Stream qui ne termine pas Pas de await responseStream.WriteAsync sur la dernière itération Boucle foreach await implicite ferme à la sortie
Mémoire qui explose en server streaming Pas de backpressure côté client Utiliser System.Threading.Channels pour réguler
Erreur « received RST_STREAM with code 0 » Idle timeout côté load balancer Configurer KeepAlive côté client + LB
Méthode async vs Async suffixe Confusion dans les noms générés Toujours utiliser la version Async côté client

Foire aux questions

gRPC ou REST pour mon API publique ?
REST pour les API publiques exposées à des clients hétérogènes (browser, mobile, intégrations tiers). gRPC pour le service-to-service interne où vous contrôlez les deux bouts.

Quelle perf vs REST ?
5-10× moins de bande passante, 10× plus rapide en sérialisation/désérialisation, latence p99 typiquement 2-3× meilleure sur des charges concurrentes.

HTTP/3 supporté ?
En preview .NET 10 via QUIC. Pour les services internes, HTTP/2 reste suffisant et plus mature. HTTP/3 brille sur Internet (mobile, perte de paquets).

Comment versionner un service ?
Versionner le package (catalogue.v1, catalogue.v2). Garder les anciens services tant que des clients dépendent. Marquer les champs deprecated dans le proto.

Comment tester ?
Tests d’intégration via WebApplicationFactory et un client gRPC pointé sur le serveur de test. Pour les tests unitaires, instancier directement le service avec mocks.

Pour aller plus loin

Le service gRPC en place, l’étape suivante consiste à mesurer la performance avec BenchmarkDotNet pour confirmer les gains. Vue panoramique : C# et .NET moderne.

Ressources et références

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité