دروس السلسلة: 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.