Mert Özen Her Satırda Daha İleri
Backend gRPC ve GraphQL — #12

gRPC ve GraphQL'de Sürümlerle Gelenler: .NET Core 3.0'dan .NET 11'e, Hot Chocolate 13'ten 16'ya

Mert Özen 14 Kas 2026 10 dk 4 görüntülenme
gRPC ve GraphQL'de Sürümlerle Gelenler: .NET Core 3.0'dan .NET 11'e, Hot Chocolate 13'ten 16'ya

Eski bir gRPC ya da GraphQL projesinde ilk bakılacak yerler Grpc.Core kullanımı ve Hot Chocolate'ın ana sürümüdür: Grpc.Core 2021'den beri bakım modunda, Hot Chocolate 16 ise scalar API'sini kökten değiştirdi. grpc-dotnet'in .NET Core 3.0'dan .NET 11'e, Hot Chocolate'ın 13'ten 16'ya getirdiklerini, aynı ISBN scalar'ının iki sürümdeki farkını ve GraphQL spesifikasyonunun September 2025 sürümünü gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; gRPC tarafında Grpc.Net.Client 2.85 ve karşılaştırma için Grpc.Core 2.46.6, GraphQL tarafında Hot Chocolate 15.1.21 ve 16.7.5 kullanılmıştır.

Önce Tahmin Et

Devraldığın bir projede Inventory'ye şöyle bağlanılıyor:

// eski: Grpc.Core, native C-core kütüphanesi üzerinden
Channel channel = new($"{host}:5301", ChannelCredentials.Insecure);
InventoryService.InventoryServiceClient client = new(channel);
ReserveStockResponse reply = await client.ReserveStockAsync(request, deadline: DateTime.UtcNow.AddSeconds(5));
Console.WriteLine($"Grpc.Core: {reply.ReservationId[..10]}...");
await channel.ShutdownAsync();

Proje .NET 10'a taşınıyor. Bu kod derlenir mi? Çalışır mı? Çalışıyorsa neden dokunasın?

Cevap: derleniyor ve x64 Linux'ta çalıştı. Ama bu, Grpc.Core'un 2021'den beri bakım modunda olduğu ve son sürümünün Ocak 2023'te çıktığı gerçeğini değiştirmiyor. Bu yazı, serinin bütün yazılarında .NET 10 ve Hot Chocolate 16 ile kurduğumuz şeylerin eski sürümlerde nasıl göründüğünü ve eski bir projeyi okurken nelere dikkat edeceğini toparlıyor.

gRPC on .NET: Grpc.Core'dan grpc-dotnet'e

.NET'te iki gRPC uygulaması oldu. İlki Grpc.Core: Google'ın C ile yazılmış gRPC çekirdeğini (C-core) saran bir kütüphane. Her platform için bir native kütüphane (libgrpc_csharp_ext) taşıyor ve HTTP/2'yi (HyperText Transfer Protocol 2) kendi çekirdeğinde konuşuyor. İkincisi .NET Core 3.0 ile gelen grpc-dotnet: server tarafı Grpc.AspNetCore ile Kestrel'in, client tarafı Grpc.Net.Client ile HttpClient'ın HTTP/2 desteğini kullanıyor; native bağımlılığı yok.

gRPC projesinin resmî blogu Mayıs 2021'de Grpc.Core'un bakım moduna girdiğini ve önerilen uygulamanın grpc-dotnet olduğunu duyurdu. Kaldırma tarihi önce Mayıs 2022, sonra Mayıs 2023 olarak ilan edildi ve sonradan en az Ekim 2024'e uzatıldı. NuGet'teki son sürüm 2.46.6, 11 Ocak 2023 tarihli. Yeni özellik gelmiyor ve native kütüphane yalnız paketin içinde derlenmiş geldiği platformlarda var.

Yeni hâli neredeyse aynı satır sayısında:

// yeni: Grpc.Net.Client, HttpClient ve .NET'in kendi HTTP/2 yığını üzerinden
using GrpcChannel channel = GrpcChannel.ForAddress($"http://{host}:5301");
InventoryService.InventoryServiceClient client = new(channel);
ReserveStockResponse reply = await client.ReserveStockAsync(request, deadline: DateTime.UtcNow.AddSeconds(5));
Console.WriteLine($"Grpc.Net.Client: {reply.ReservationId[..10]}...");
Grpc.Core: ord-legacy...
Grpc.Net.Client: ord-legacy...

Üretilen client sınıfı (InventoryServiceClient) iki uygulamada da aynı; Grpc.Tools aynı .proto'dan aynı kodu üretiyor. Değişen yalnız channel. Göç bu yüzden çoğu projede küçük: Channel yerine GrpcChannel.ForAddress, adreste şema (http:// ya da https://), ShutdownAsync yerine Dispose. Dikkat edilmesi gereken farklar channel seçeneklerinde: Grpc.Core'un string anahtarlı ChannelOption listesi, grpc-dotnet'te GrpcChannelOptions'ın tipli property'leri oldu (mesaj boyutu limitleri, HttpHandler, credentials). Server tarafında ise Grpc.Core.Server ile yazılmış kendi host'unu ASP.NET Core'a taşımak gerekiyor; serinin ilk gRPC service yazısındaki AddGrpc/MapGrpcService düzeni.

grpc-dotnet'in sürüm sürüm getirdikleri, serinin hangi yazısında kullandığımızla birlikte:

SürümNe geldiSerideki yeri
.NET Core 3.0 (2019)Grpc.AspNetCore ve Grpc.Net.Client; Kestrel'de gRPCİlk gRPC service ve client
Grpc.AspNetCore.Web 2.29 (Mayıs 2020)gRPC-Web için ilk kararlı paketTarayıcıdan gRPC
Grpc.Net.Client 2.36 (Mart 2021)Service config ile retry ve hedgingProduction'da gRPC
Grpc.Net.Client 2.45 (Nisan 2022), .NET 5+Client-side load balancing: dns:/// resolver, round_robinProduction'da gRPC
.NET 7 (Kasım 2022)JSON (JavaScript Object Notation) transcoding (Microsoft.AspNetCore.Grpc.JsonTranscoding)Tarayıcıdan gRPC
.NET 8gRPC client ve server için Native AOT (uygulama çalışmadan önce makine koduna derleme) desteği
Grpc.Net.ClientFactory 2.64 (Temmuz 2024)AddStandardResilienceHandler ile uyumProduction'da gRPC
.NET 11 (Kasım 2026)Microsoft.AspNetCore.Grpc.Swagger deprecatedTarayıcıdan gRPC

Son satırın pratik karşılığını eski bir projede görmenin en hızlı yolu NuGet'in deprecation bilgisini sormak:

dotnet list package --deprecated

Project `LegacyGrpc` has the following deprecated packages
   [net10.0]:
   Top-level Package                        Requested   Resolved   Reason(s)   Alternative
   > Microsoft.AspNetCore.Grpc.Swagger      0.10.11     0.10.11    Other                  

NuGet'teki deprecation mesajı paketin artık bakımının yapılmadığını söylüyor ve bir alternatif önermiyor. Grpc.Core bu listede çıkmadı: bakım modunda olması NuGet'te deprecation olarak işaretlenmiş olduğu anlamına gelmiyor. Bu yüzden komutun çıktısı tek başına yeterli bir kontrol listesi değil.

Hot Chocolate: 13'ten 16'ya

Hot Chocolate'ın son dört ana sürümü NuGet'te 7 Şubat 2023 (13), 16 Ekim 2024 (14), 4 Şubat 2025 (15) ve 30 Nisan 2026 (16) tarihli. Bu seride kullandığımız her şeyin bir geçmişi var:

  • 13: GraphQL over HTTP'nin medya tipi (application/graphql-response+json) varsayılan oldu, request batching varsayılan olarak kapandı, @defer/@stream EnableDefer/EnableStream arkasına alındı, subscription topic'leri string oldu ve AddInMemorySubscriptions schema builder'a taşındı.
  • 14: complexity analyzer yerini IBM'in Cost spesifikasyonu taslağına dayanan, varsayılan olarak açık cost analysis'e bıraktı; service'ler [Service] olmadan enjekte edilir oldu, query resolver'ları varsayılan olarak resolver scope'una geçti, RegisterDbContext yerini RegisterDbContextFactory'ye bıraktı, Banana Cake Pop'un adı Nitro oldu, "persisted query" adı "persisted operation" oldu.
  • 15: .NET Standard 2.0, .NET 6 ve .NET 7 desteği kalktı; DataLoader'ları elle kaydetmek yerine source-generated DataLoader önerilir oldu; DateOnly ve TimeOnly kendi scalar'larına bağlandı.
  • 16: yeni scalar API'si, [BatchResolver], onError, incremental delivery v0.2, semantic introspection (__search), [RequiresOptIn], GraphQL semantic convention'larına uyan OpenTelemetry span'ları, MCP (Model Context Protocol; AI modellerini tool'lara ve veri kaynaklarına bağlayan protokol) ve OpenAPI adapter'ları; schema'nın açılışta kurulması ve schema service'lerinin uygulama service'lerinden ayrılması.

Serinin önceki yazılarında 16'nın bu maddelerin çoğuyla karşılaştık: [BatchResolver]'ın iki root field'lı sorguda takılması, error filter'ın AddApplicationService istemesi, @defer'ın yeni wire formatı. Göç rehberinin (15'ten 16'ya) en uzun bölümleri de bunlar; ayrıca filter argümanı başına 64 operasyon sınırı, fieldCoordinate hata extension'ının coordinate olması ve OpenTelemetry span adlarının değişmesi gibi davranışı sessizce değiştiren maddeler var. Dashboard'ların ve alarm kurallarının span adlarına bağlı olduğu bir sistemde bu son madde bir göç maddesi kadar bir operasyon maddesi.

Scalar API'si: Parse/Serialize'dan Coerce'a

Eski bir Hot Chocolate projesinde en çok kırılacak yer custom scalar'lar. Bookstore'da ISBN için bir scalar yazdım: tireleri atıyor, 978 ya da 979 ile başlayan 13 hane bekliyor. Hot Chocolate 15 ile:

// Hot Chocolate 15 ve öncesi: Parse*/Serialize ailesi
public sealed class IsbnType() : ScalarType<string, StringValueNode>("Isbn")
{
    protected override bool IsInstanceOfType(StringValueNode valueSyntax) =>
        Isbn.Normalize(valueSyntax.Value) is not null;

    protected override string ParseLiteral(StringValueNode valueSyntax) =>
        Isbn.Normalize(valueSyntax.Value) ?? throw new SerializationException("Invalid ISBN.", this);

    protected override StringValueNode ParseValue(string runtimeValue) => new(runtimeValue);

    public override IValueNode ParseResult(object? resultValue) =>
        resultValue is string s ? new StringValueNode(s) : NullValueNode.Default;

    public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
    {
        runtimeValue = resultValue is string s ? Isbn.Normalize(s) : null;
        return runtimeValue is not null;
    }
}

Aynı sınıf Hot Chocolate 16.7.5'e karşı derlenmedi. Beş override'ın beşi de CS0115 ("no suitable method found to override") verdi; eski method'lar taban sınıfta yok. Üstüne dört yeni soyut üye için CS0534 geldi: OnCoerceInputLiteral(StringValueNode), OnCoerceInputValue(JsonElement, IFeatureProvider), OnCoerceOutputValue(string, ResultElement) ve OnValueToLiteral(string). Yeni hâli:

// Hot Chocolate 16: dört coerce/literal method'u
public sealed class IsbnType() : ScalarType<string, StringValueNode>("Isbn")
{
    protected override string OnCoerceInputLiteral(StringValueNode valueLiteral) =>
        Isbn.Normalize(valueLiteral.Value) ?? throw CreateCoerceInputLiteralError(valueLiteral);

    protected override string OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) =>
        Isbn.Normalize(inputValue.GetString()) ?? throw CreateCoerceInputValueError(inputValue);

    protected override void OnCoerceOutputValue(string runtimeValue, ResultElement resultValue) =>
        resultValue.SetStringValue(runtimeValue);

    protected override StringValueNode OnValueToLiteral(string runtimeValue) => new(runtimeValue);
}

Adlar spesifikasyonun dilini izliyor: sorgu metnindeki bir literal'i (OnCoerceInputLiteral) ve JSON variable'ı (OnCoerceInputValue) ayrı ayrı çözüyorsun; çıktıyı bir object döndürerek değil, doğrudan yanıtın JSON yapısına (ResultElement) yazarak üretiyorsun. Variable'lar artık System.Text.Json'ın JsonElement'i olarak geliyor; ara bir sözlük ya da object ağacı yok. Aynı sorgular iki sürümde:

{ bookByIsbn(isbn: "978-0000000001") { isbn title } }
15: {"data":{"bookByIsbn":{"isbn":"9780000000001","title":"Distributed Systems Vol. 1"}}}
16: {"data":{"bookByIsbn":{"isbn":"9780000000001","title":"Distributed Systems Vol. 1"}}}

{ bookByIsbn(isbn: "12345") { title } }
15: {"errors":[{"message":"The specified argument value does not match the argument type.","locations":[{"line":1,"column":20}],"path":["bookByIsbn"],"extensions":{"argument":"isbn","argumentValue":"\"12345\"","locationType":"Isbn!","specifiedBy":"https://spec.graphql.org/October2021/#sec-Values-of-Correct-Type"}}]}
16: {"errors":[{"message":"Isbn cannot coerce the given literal of type `StringValue` to a runtime value.","path":["bookByIsbn"],"extensions":{"inputPath":["isbn"],"coordinate":"Query.bookByIsbn(isbn:)","fieldType":"Isbn"}}],"data":{"bookByIsbn":null}}

query($i: Isbn!) { bookByIsbn(isbn: $i) { title } }   variables: {"i":"978-x"}
15: {"errors":[{"message":"Isbn cannot parse the given literal of type `StringValueNode`.","path":["i"]}]}
16: {"errors":[{"message":"Isbn cannot coerce the given value JSON element of type `String` to a runtime value.","extensions":{"inputPath":["i"],"fieldType":"Isbn"}}]}

Geçerli değerde fark yok. Geçersiz literal'de davranış değişti: 15'te IsInstanceOfType sayesinde doğrulama aşamasında reddedilip data hiç dönmedi; 16'da hata coerce sırasında, field düzeyinde oluştu ve data içinde bookByIsbn: null ile birlikte geldi. Hata mesajı ve extension'lar da değişti (inputPath, coordinate). Client'ların hata mesajlarını metin olarak eşleştirdiği bir sistemde scalar göçünü test etmeden yapma. 15'teki specifiedBy adresinin October 2021 spesifikasyonunu, serinin önceki yazılarındaki 16 çıktılarının September 2025'i gösterdiğine de dikkat et.

GraphQL Spesifikasyonu: October 2021'den September 2025'e

Spesifikasyonun September 2025 sürümü, October 2021'den sonraki ilk resmî sürüm. Serinin GraphQL temelleri yazısında kullandığımız eklemeler: @oneOf input object'leri, argument ve input field'larda @deprecated, operation ve fragment'larda description, schema coordinate'ları ve tam Unicode desteği. Dışarıda kalanlar da en az bunlar kadar önemli: @defer ve @stream hâlâ spesifikasyonda değil ve wire formatları (v0.1'den v0.2'ye) çalışma grubunda değişiyor; GraphQL over HTTP hâlâ taslak; birden çok servisin schema'sını birleştirme işi (Composite Schemas) ayrı bir spesifikasyon çalışması olarak sürüyor; onError ve @requiresOptIn öneri aşamasında. Hot Chocolate 16 bunların hepsini uyguluyor; bu yüzden "Hot Chocolate destekliyor" ile "spesifikasyonda var" aynı şey değil. Client tarafında bir özelliğe güvenmeden önce bu ayrımı yap.

Eski Bir Projeyi Okumak

Devraldığın bir gRPC ya da GraphQL projesinde ilk yarım saatte bakacağım yerler:

  • Paketler: dotnet list package --deprecated ve --outdated; referanslarda Grpc.Core var mı (yalnız Grpc.Core.Api görmek sorun değil, o grpc-dotnet'in de kullandığı managed API paketi); bütün HotChocolate.* paketleri aynı sürümde mi.
  • gRPC client'ları: new Channel(...) ve ChannelOption görürsen Grpc.Core; her çağrıda yeni channel ve deadline'sız çağrılar serinin ilk gRPC yazısındaki tuzaklar.
  • Hot Chocolate 12–13 kodu: [Service] ve [ScopedService] attribute'ları, RegisterDbContext<T>(DbContextKind.Pooled), [SubscribeAndResolve], IQueryResult; bunların hepsinin karşılığı 14 ve sonrasında değişti.
  • Scalar'lar: ParseLiteral, ParseValue, Serialize, TryDeserialize override'ları 16'ya geçişte yeniden yazılacak kod demek.
  • Kütüphane: using GraphQL; ve GraphQL.NewtonsoftJson paketini görürsen proje Hot Chocolate değil, GraphQL.NET kullanıyor; o paket GraphQL.NET'in Newtonsoft.Json tabanlı serializer'ı. API'leri tamamen farklı, bu serideki kod oraya doğrudan taşınmaz.
  • Güvenlik varsayılanları: cost analysis 14 ile varsayılan hâle geldi; daha eski bir sürümde hangi analizlerin açık olduğunu, introspection ve hata ayrıntısı ayarlarını açıkça kontrol et.

Üretimde Dikkat

Ana sürüm geçişlerini tek adımda yapma. Hot Chocolate'ta 13'ten 16'ya atlamak yerine her ana sürümün göç rehberini sırayla uygula; her adımda schema'yı dışa aktarıp bir önceki adımla diff'le. Serinin güvenli işletme yazısındaki schema diff'i burada da işe yarıyor: göç bir şeyi istemeden değiştirdiyse diff gösterir.

Observability'yi göçün parçası say. Span adları, metric adları ve hata kodları değişebilir; dashboard ve alarm kurallarını yeni sürümle aynı ortamda doğrula.

Bakım modundaki bağımlılıkları envantere koy. Grpc.Core gibi bir paket bugün çalışıyor olabilir; ama güvenlik düzeltmesi gelmeyen ve yeni platformlar için native kütüphanesi derlenmeyen bir bağımlılık zamanla bir risk kalemine dönüşür.

Sık Düşülen Tuzaklar

"Derleniyor, o zaman sorun yok" demek. Grpc.Core .NET 10'da derleniyor; bakım modunda olduğu gerçeği derleyiciye yansımıyor.

NuGet deprecation listesini tek ölçü saymak. Bakım modundaki bir paket deprecated olarak işaretlenmemiş olabilir.

Scalar göçünü yalnız derlemeyle doğrulamak. Hata mesajları, hatanın oluştuğu aşama ve data'nın varlığı değişebiliyor; client'larla birlikte test et.

Hot Chocolate'ın bir özelliğini GraphQL spesifikasyonu sanmak. @defer, onError ve semantic introspection başka bir server'da yok ya da farklı çalışabilir.

Küçük Bir Deneme

★☆☆ Kendi projelerinden birinde dotnet list package --deprecated ve --outdated çalıştır; çıkan her paket için bakım durumunu paketin deposundan kontrol et.

★★☆ Bu yazıdaki IsbnType'ın 16 sürümüne, geçersiz ISBN'de daha açıklayıcı bir mesaj üreten bir hata ekle (CreateCoerceInputLiteralError'ı override ederek) ve literal ile variable yollarının ikisinde de mesajı doğrula.

★★★ Grpc.Core kullanan küçük bir server'ı (Grpc.Core.Server) ASP.NET Core'a taşı: aynı .proto, aynı service implementasyonu; değişen yalnız host. Deadline, metadata ve status code davranışlarının iki hâlde aynı kaldığını bir test client'ıyla göster.

Seriyi burada kapatıyoruz. Protocol Buffers contract'larından başlayıp gRPC'yi production'a, GraphQL'i Hot Chocolate 16 ile Angular'a kadar taşıdık ve üç API stilini aynı senaryoda ölçtük. Bookstore'un service'lerinin gRPC ile uçtan uca nasıl konuştuğunu ilerideki .NET ile Mikroservis Uygulaması serisinde, gerçek bir dağıtık sistem içinde göreceğiz.