تطوير الويب

gRPC مع ASP.NET Core 10: خدمات، streaming وclients مُنمَّطة

5 min de lecture

دروس السلسلة: C# 14 و.NET 10 · EF Core 10 · Worker Services .NET 10 · BenchmarkDotNet .NET 10

أصبح gRPC في 2026 البروتوكول المُفضَّل لاتصالات inter-microservices عالية الأداء في منظومة .NET. ASP.NET Core 10 يُوفّر دعمًا من الدرجة الأولى عبر package Grpc.AspNetCore، مبني على Protobuf وHTTP/2 (HTTP/3 في preview). مقارنةً بـ API REST JSON مكافئة، gRPC يُقدّم 5-10× أقل عرض نطاق، تسلسلات أسرع 10×، وtyping صارم ثنائي مُولَّد من جانبي client وserver. للخدمات الداخلية التي تتبادل ملايين الرسائل يوميًا، هذا الخيار الافتراضي.

المتطلبات

  • .NET 10 LTS
  • أساسيات ASP.NET Core وC# الحديث
  • أداة grpcurl أو grpcui للاختبارات
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — إنشاء مشروع gRPC

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

الملف .csproj يُشير تلقائيًا إلى Grpc.AspNetCore ويُعدّ توليد كود Protobuf عند التجميع. أصناف C# المُقابلة للرسائل والخدمات تُولَّد في obj/ عند البناء.

الخطوة 2 — تعريف عقد .proto

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;
}

أربعة أنماط methods RPC. Unary (ObtenirProduit): طلب بسيط، رد بسيط. Server streaming (ListerProduits): الخادم يُرجع تدفّقًا — مفيد لـ pagination، تصدير، logs الزمن الحقيقي. Client streaming (CreerProduits): العميل يُرسل تدفّقًا — مفيد لـ uploads بـ chunks. Bidirectional streaming (Discuter): تدفّق في الاتجاهين — مفيد لـ chat والتعاون الزمن الحقيقي.

الخطوة 3 — تنفيذ الخدمة على جانب الخادم

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);
        }
    }
}

الـ ServerCallContext يُتيح وصولًا لـ CancellationToken، metadata HTTP/2، المصادقة، وdeadline المُعَدّ من العميل. دائمًا مرّر token لعمليات await. الـ RpcException تحمل StatusCode قياسي gRPC (NotFound، InvalidArgument، Unauthenticated).

الخطوة 4 — تسجيل الخدمة في Program.cs

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();

EnableDetailedErrors في dev يعرض stack trace. MaxReceiveMessageSize 16 MB يُغطّي payloads نموذجية. gRPC reflection يُتيح لأدوات مثل grpcurl اكتشاف الخدمات دون .proto.

الخطوة 5 — Client gRPC: توليد واستدعاءات

<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>

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!;
        }
    }
}

ثلاثة انضباطات. الـ deadline مكافئ timeout على جانب العميل — مُنتشر للخادم عبر metadata. EnableMultipleHttp2Connections يتجنّب bottleneck اتصال HTTP/2 وحيد. KeepAlive يُبقي الاتصال مفتوحًا فوق انقطاعات NAT.

الخطوة 6 — Interceptors (مكافئ middleware)

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;
        }
    }
}

هكذا نحقن token JWT، نُعيد المحاولة على خطأ عابر (Unavailable، DeadlineExceeded)، أو نُجَهِّز OpenTelemetry.

الخطوة 7 — مصادقة JWT

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

[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);
    }
}

للخدمات mTLS الداخلية (zero-trust)، النمط البديل يقتضي certificat client مُتحقَّق منه بـ AddCertificate(). الاثنان يتعايشان: JWT للمستخدمين/التطبيقات، mTLS لـ service-to-service بقيمة عالية.

الخطوة 8 — gRPC-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) => { /* ... */ });

overhead gRPC-Web مقارنةً بـ gRPC الأصلي هو 10-20% (encodage base64). للـ SPA/PWA التي تستهلك microservices مُنمَّطة، بديل ممتاز لـ REST.

أخطاء شائعة

العَرَض السبب الحل
خطأ « HTTP/2 over TLS » Kestrel بدون certificat HTTPS فعّل dev cert: dotnet dev-certs https --trust
StatusCode.Unimplemented على جانب العميل service غير mapped أو اسم method غير صحيح تحقّق من MapGrpcService وإعادة توليد .proto
Stream لا ينتهي لا await responseStream.WriteAsync على آخر iteration foreach await ضمنية تُغلق عند الخروج
Memory ينفجر في server streaming لا backpressure من العميل استخدم System.Threading.Channels
خطأ « received RST_STREAM with code 0 » Idle timeout على load balancer أعدّ KeepAlive من العميل + LB
method async vs Async suffix اختلاط في الأسماء المُولَّدة دائمًا استخدم نسخة Async من العميل

الأسئلة الشائعة

gRPC أم REST لـ API العامة؟
REST للـ APIs العامة المكشوفة لعملاء متغايرين. gRPC للـ service-to-service الداخلي حيث تتحكّم في الطرفين.

أي أداء مقارنةً بـ REST؟
5-10× أقل عرض نطاق، أسرع 10× في التسلسل، latency p99 نموذجيًا أفضل 2-3×.

HTTP/3 مدعوم؟
في preview .NET 10 عبر QUIC. للخدمات الداخلية، HTTP/2 يبقى كافيًا. HTTP/3 يتألّق على الإنترنت (جوّال، فقدان حزم).

كيف ننسّخ خدمة؟
إصدار package (catalogue.v1، catalogue.v2). احتفظ بالخدمات القديمة طالما يعتمد عليها عملاء. علّم الحقول deprecated في proto.

كيف نختبر؟
اختبارات تكاملية عبر WebApplicationFactory وclient gRPC مُوجَّه لخادم اختبار. لاختبارات وحدوية، أنشئ الخدمة مباشرة مع mocks.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité