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

GraphQL'in Temelleri: Schema, Query, Mutation, Subscription ve Resolver Mantığı

Mert Özen 14 Kas 2026 13 dk 4 görüntülenme
GraphQL'in Temelleri: Schema, Query, Mutation, Subscription ve Resolver Mantığı

GraphQL'de client ekranın ihtiyacını tek bir query ile tanımlar, server her field'ı kendi resolver'ıyla çözer; bu over-fetching'i bitirir ama N+1'i ve HTTP cache kaybını da beraberinde getirir. Hot Chocolate 16 ile type system'i, variable/fragment/alias'ı, partial data ve null propagation'ı, September 2025 spesifikasyonunun @oneOf'unu ve GraphQL over HTTP davranışını gerçek yanıtlarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ve Hot Chocolate 16.7 ile derlenip denenmiştir; spesifikasyon atıfları GraphQL September 2025 sürümüne ve GraphQL over HTTP taslağına göredir.

Önce Tahmin Et

Ana sayfada beş kitaplık bir liste var: başlık ve stok durumu. Stok bilgisi Inventory'den geliyor ve dördüncü kitabın sorgusu sırasında Inventory hata veriyor. Client GraphQL'den ne alır? HTTP 500 mü? Dört kitap ve bir hata mesajı mı? Yoksa hiç kitap mı?

Cevap tek bir karaktere bağlı: schema'da stok alanının sonunda ! olup olmamasına. İkisini de gerçek yanıtlarıyla göreceğiz.

GraphQL Hangi Problemi Çözer?

Bookstore'un kitap detay ekranında dört şey var: kitabın başlığı ve fiyatı, yazarın adı, stok durumu ve son iki yorum. REST (Representational State Transfer) ile bunun için dört endpoint çağırdım:

GET /api/books/1  96 bytes
{"id":1,"title":"Distributed Systems Primer","isbn":"9780000000001","authorId":1,"price":249.90}
GET /api/authors/1  69 bytes
{"id":1,"name":"Ada Byron","bio":"Writes about distributed systems."}
GET /api/books/1/stock  27 bytes
{"available":12,"status":0}
GET /api/books/1/reviews  300 bytes
[{"id":3,"bookId":1,"rating":3,"text":"A bit long.","createdAt":"2026-11-09T12:00:00+00:00"},{"id":2,"bookId":1,"rating":4,"text":"Great chapter on retries.","createdAt":"2026-11-08T12:00:00+00:00"},{"id":1,"bookId":1,"rating":5,"text":"Clear and practical.","createdAt":"2026-11-07T12:00:00+00:00"}]
total 492

İki ayrı sorun var. Over-fetching: ekran isbn, bio, createdAt ya da üçüncü yorumu kullanmıyor, ama hepsi geliyor. Under-fetching: tek bir istek ekranı doldurmaya yetmiyor; üstelik yazarı istemek için önce kitabın authorId'sini bilmek gerekiyor, yani en az iki ardışık gidiş-dönüş var. Mobil ağda her gidiş-dönüş yüzlerce milisaniye demek.

GraphQL'de ekran neye ihtiyacı olduğunu kendisi söyler:

"""Book detail page: book, author, stock and the latest reviews."""
query BookDetail($id: Int!) {
  book(by: { id: $id }) {
    title
    price
    author {
      name
    }
    stock {
      available
      status
    }
    reviews(first: 2) {
      rating
      text
    }
  }
}
HTTP/1.1 200 OK
Content-Length: 240
Content-Type: application/graphql-response+json; charset=utf-8

{"data":{"book":{"title":"Distributed Systems Primer","price":249.90,"author":{"name":"Ada Byron"},"stock":{"available":12,"status":"IN_STOCK"},"reviews":[{"rating":3,"text":"A bit long."},{"rating":4,"text":"Great chapter on retries."}]}}}

Tek istek, 240 byte, yanıtın şekli sorgunun şekliyle birebir aynı. Bu esnekliğin bedelleri de var ve onları baştan bilmek gerekiyor. Bütün istekler tek bir endpoint'e (/graphql) gider ve çoğu POST'tur; URL başına çalışan HTTP cache'i, CDN (statik dosyaları kullanıcıya yakın sunuculardan dağıtan ağ) ve "hangi endpoint yavaş" diyen monitoring araçları kör kalır. Client istediği sorguyu yazabildiği için server'ın maliyeti tahmin edilemez hâle gelir. REST'in yıllar içinde oturmuş araçları burada yeniden kurulmak zorunda. Bu bedellerin nasıl yönetildiğini serinin güvenlik ve işletme yazısında, REST ile karşılaştırmasını da seri sonundaki ölçüm yazısında ele alıyoruz.

Schema, Type System ve Introspection

GraphQL API'nin contract'ı schema'dır. gRPC'deki .proto'nun karşılığı, ama farkla: client schema'yı server'dan sorgulayabilir. Bookstore Catalog service'inin schema'sını Hot Chocolate'ın /graphql?sdl adresinden SDL (GraphQL şemasını yazmak için kullanılan dil) olarak aldım:

type Query {
  book(by: BookLookupInput!): Book
  books(first: Int! = 10): [Book!]!
  search(term: String!): [SearchResult!]!
}

type Mutation {
  addReview(bookId: Int!, rating: Int!, text: String!): Review!
    @cost(weight: "10")
}

type Subscription {
  onReviewAdded(bookId: Int!): Review!
}

type Book {
  author: Author
  stock: StockInfo @cost(weight: "10")
  reviews(first: Int! = 3, limit: Int @deprecated(reason: "Use first.")): [Review!]!
  id: Int!
  title: String!
  isbn: String!
  authorId: Int!
  price: Decimal!
}

type StockInfo {
  available: Int!
  status: StockStatus!
}

union SearchResult = Book | Author

input BookLookupInput @oneOf {
  id: Int
  isbn: String
}

enum StockStatus {
  IN_STOCK
  LOW
  OUT_OF_STOCK
}

scalar Decimal
  @specifiedBy(url: "https://scalars.graphql.org/chillicream/decimal.html")

Bu kısa schema type system'in neredeyse tamamını içeriyor. type object tipleri tanımlar ve her field'ı başka bir tipe bağlar. Ağaç sonunda scalar'larda biter: Int, String, Boolean, Float, ID yerleşik scalar'lar. Decimal ve DateTime ise Hot Chocolate'ın custom scalar'ları; @specifiedBy davranışlarının nerede tanımlandığını gösteriyor. enum sabit değer kümesi; C#'taki StockStatus.InStock GraphQL'de IN_STOCK oldu. union "bunlardan biri" demek; search kitap da dönebilir yazar da. interface ise ortak field'ları olan tiplerin sözleşmesi (örneğin bir Product interface'i ve onu implement eden Book ile EBook); burada kullanmadık ama sorgulanma biçimi union'la aynı. input yalnız argüman olarak kullanılabilen tiplerdir; output tipleri argümanda, input tipleri yanıtta kullanılamaz.

Ünlem işaretleri ve köşeli parantezler en çok yanlış okunan kısım. Book "null olabilir", Book! "asla null değil" demek. [Book!]! iki ayrı garanti veriyor: liste kendisi null olmaz, içindeki hiçbir kitap da null olmaz. GraphQL'de varsayılan nullable'dır; C#'ın tersine. Baştaki sorunun cevabı da tam bu işaretlerde saklı.

Schema'yı C# kodu üretiyor. Hot Chocolate'ın implementation-first yaklaşımında attribute'larla işaretlenmiş static class'lar yazıyorsun, source generator kalanını hallediyor:

[OneOf]
public sealed record BookLookupInput(int? Id, string? Isbn);

[QueryType]
public static partial class Queries
{
    public static Book? GetBook(BookLookupInput by, CatalogData data) =>
        by.Id is { } id ? data.FindBook(id) : data.FindBookByIsbn(by.Isbn!);

    public static IReadOnlyList<Book> GetBooks(CatalogData data, int first = 10) => data.ListBooks(first);

    public static IReadOnlyList<ISearchResult> Search(string term, CatalogData data) => data.Search(term);
}

[ObjectType<Book>]
public static partial class BookNode
{
    public static Author? GetAuthor([Parent] Book book, CatalogData data) => data.FindAuthor(book.AuthorId);

    // nullable: Inventory'ye ulaşılamazsa yalnız bu alan null olur, kitap kaybolmaz
    public static async Task<StockInfo?> GetStock([Parent] Book book, StockClient stock, CancellationToken cancellationToken) =>
        await stock.GetStockAsync(book.Id, cancellationToken);

    public static IReadOnlyList<Review> GetReviews(
        [Parent] Book book,
        CatalogData data,
        int first = 3,
        [GraphQLDeprecated("Use first.")] int? limit = null) =>
        data.ListReviews(book.Id, limit ?? first);
}
builder.Services
    .AddGraphQLServer()
    .AddCatalogTypes()
    .AddInMemorySubscriptions();

var app = builder.Build();

app.MapGraphQL();

AddCatalogTypes source generator'ın assembly adından ürettiği kayıt method'u. C# nullable annotation'ları schema'ya doğrudan yansıyor: Book? → Book, IReadOnlyList<Book> → [Book!]!. Hot Chocolate'ın üç farklı schema yazma yaklaşımı, Entity Framework Core (EF Core) entegrasyonu ve mutation convention'ları bir sonraki yazının konusu. Bu yazıda yalnız kavramlara yetecek kadarını kullanıyoruz.

Schema'nın kendisi de sorgulanabilir. Buna introspection denir; __schema ve __type gibi özel field'larla yapılır. IDE'lerdeki otomatik tamamlama ve kod üreticiler bunu kullanır:

{ __type(name: "StockStatus") { kind enumValues { name } } }

{"data":{"__type":{"kind":"ENUM","enumValues":[{"name":"IN_STOCK"},{"name":"LOW"},{"name":"OUT_OF_STOCK"}]}}}

Aynı sorguyu uygulama Production ortamında çalışırken attığımda Hot Chocolate 16 reddetti: Introspection is not allowed for the current request. (HC0046). Introspection Development'ta açık, Production'da varsayılan olarak kapalı geliyor. Production'da kimin introspection yapabileceği kararını güvenlik yazısında ele alıyoruz.

Query Yazmak: Argument, Variable, Fragment ve Alias

Baştaki BookDetail sorgusunda üç temel parça vardı. by: { id: $id } bir argument. $id bir variable; değeri sorgu metninin içine gömülmez, ayrı bir JSON (JavaScript Object Notation) olarak gönderilir ({"id": 1}). Bu sayede sorgu metni sabit kalır, server onu bir kez parse edip önbelleğe alabilir. Kullanıcı girdisi metne string birleştirmeyle eklenmediği için de injection riski olmaz. BookDetail adı operation name; loglarda ve trace'lerde hangi ekranın hangi sorguyu attığını gösterir.

İki kitabı yan yana karşılaştıran bir ekranda aynı field'ı iki kez istemek gerekir. Alias'lar yanıt alanına isim verir, fragment'lar tekrar eden seçimi tek yerde toplar:

query Compare($a: Int!, $b: Int!) {
  left: book(by: { id: $a }) { ...BookCard }
  right: book(by: { id: $b }) { ...BookCard }
}

fragment BookCard on Book {
  title
  price
  stock { status }
}
{"data":{"left":{"title":"Distributed Systems Primer","price":249.90,"stock":{"status":"IN_STOCK"}},"right":{"title":"Practical Protobuf","price":189.00,"stock":{"status":"LOW"}}}}

Union dönen bir alanda hangi tipten hangi field'ların isteneceğini inline fragment'lar söyler. __typename her object'in gerçek tipini verir:

{
  search(term: "ra") {
    __typename
    ... on Book { title }
    ... on Author { name }
  }
}
{"data":{"search":[{"__typename":"Book","title":"Practical Protobuf"},{"__typename":"Book","title":"Streaming in Practice"},{"__typename":"Book","title":"GraphQL from Scratch"},{"__typename":"Author","name":"Grace Hopper"}]}}

Angular tarafında her component kendi ihtiyacını bir fragment olarak tanımlar, sayfa sorgusu bu fragment'ları birleştirir. Bu düzeni Angular client yazısında kuruyoruz.

Mutation ve Subscription

Query okuma içindir. Veriyi değiştiren işlemler mutation olarak tanımlanır; fark syntax'tan çok semantikte. Spesifikasyona göre bir istekteki birden çok top-level mutation field'ı sırayla çalışır, query field'ları ise paralel çalışabilir. Bir yorum ekleyen mutation ve o kitabın yeni yorumlarını dinleyen bir subscription:

[MutationType]
public static partial class Mutations
{
    public static async Task<Review> AddReview(
        int bookId, int rating, string text, CatalogData data, HotChocolate.Subscriptions.ITopicEventSender sender, CancellationToken cancellationToken)
    {
        Review review = data.AddReview(bookId, rating, text);
        await sender.SendAsync($"reviews/{bookId}", review, cancellationToken);
        return review;
    }
}

[SubscriptionType]
public static partial class Subscriptions
{
    [Subscribe]
    [Topic("reviews/{bookId}")]
    public static Review OnReviewAdded(int bookId, [EventMessage] Review review) => review;
}

Subscription uzun ömürlü bir bağlantı ister. Hot Chocolate hem WebSocket (graphql-transport-ws protokolü) hem SSE (sunucudan istemciye tek yönlü event akışı) destekliyor. SSE ile denedim: bir terminalde Accept: text/event-stream ile subscription'ı açtım, diğerinde mutation'ı gönderdim:

mutation { addReview(bookId: 3, rating: 4, text: "Good examples.") { id rating } }

{"data":{"addReview":{"id":6,"rating":4}}}

event: next
data: {"data":{"onReviewAdded":{"rating":4,"text":"Good examples."}}}

Subscription'ın üretimdeki ayarlarını (Redis provider, birden çok instance, ne zaman SignalR'ın daha doğru olduğu) Angular client yazısında konuşuyoruz. Real-time Uygulamalar serisinin ilk yazısındaki seçenekler haritası burada da geçerli.

Execution: Resolver Ağacı ve N+1'in Doğduğu Yer

Server bir sorguyu nasıl çalıştırır? Önce parse eder, schema'ya göre doğrular, sonra seçim kümesini (selection set) yukarıdan aşağı yürür. Her field'ın bir resolver'ı vardır; resolver parent object'i alır ve o field'ın değerini döner. BookDetail sorgusu çalışırken veri katmanının logu:

06-resolver-tree-tr.png

info: Bookstore.Catalog.Data.CatalogData[0]
      db: book 1
info: Bookstore.Catalog.Data.CatalogData[0]
      db: author 1
info: Bookstore.Catalog.Data.CatalogData[0]
      db: reviews of book 1
info: Bookstore.Catalog.Data.StockClient[0]
      grpc: Inventory.GetStock 1

Önce Query.book çalıştı. Onun döndürdüğü Book object'i, alt field'ların parent'ı oldu. title ve price için ayrı bir I/O yok, değer zaten parent'ta. author, reviews ve stock ise kendi veri kaynaklarına gitti; biri veritabanına, biri gRPC ile Inventory'ye. GraphQL server'ı bu kaynakları birleştiren bir katman ve her field kendi kaynağından bağımsız olarak çözülebiliyor.

Bu model aynı zamanda N+1 probleminin kaynağı. Beş kitabın yazarlarını isteyen { books(first: 5) { title author { name } } } sorgusunun logu:

db: books first 5
db: author 1
db: author 1
db: author 2
db: author 3
db: author 2

Bir sorgu kitapları getirdi, sonra author resolver'ı her kitap için ayrı ayrı çalıştı: 1 + 5 sorgu. Ada Byron'ı iki kez, Alan Turing'i iki kez sorduk. EF Core serisinin N+1 yazısında aynı problemi navigation property'lerde görmüştük. GraphQL'de fark etmek daha zor, çünkü hangi field'ların isteneceğini client belirliyor; 500 kitaplık bir liste 501 sorgu demek. Çözümü DataLoader ve batch resolver'lar; GraphQL performans yazısında bu 1 + 5'i 2'ye indiriyoruz.

Hatalar: Partial Data ve Null Propagation

Baştaki soruya dönelim. Inventory dördüncü kitap için exception fırlatıyor. Schema'nın ilk hâlinde stok alanı non-null'dı: C#'ta Task<StockInfo>, schema'da stock: StockInfo!. Beş kitabı stoklarıyla istedim:

{
  books(first: 5) {
    title
    stock { available }
  }
}
200 application/graphql-response+json; charset=utf-8
{"errors":[{"message":"Unexpected Execution Error","path":["books",3,"stock"]}],"data":null}

Hiç kitap yok, data null. Tek bir stok hatası bütün listeyi götürdü. Sebebi null propagation: stock null olamayacağı için hata bir üst seviyeye, kitaba taşındı. Ama kitap da [Book!]! içinde non-null; hata listeye taşındı. Liste de non-null olduğu için en sonunda data'nın kendisi null oldu. GraphQL "null olmayacak" garantisini korumak için yanıtı en yakın nullable üst alana kadar budar.

Resolver'ı Task<StockInfo?> yapınca schema'da stock: StockInfo oldu ve aynı sorgu:

200 application/graphql-response+json; charset=utf-8
{"errors":[{"message":"Unexpected Execution Error","path":["books",3,"stock"]}],"data":{"books":[{"title":"Distributed Systems Primer","stock":{"available":12}},{"title":"Practical Protobuf","stock":{"available":1}},{"title":"Streaming in Practice","stock":{"available":12}},{"title":"Compilers for Everyone","stock":null},{"title":"GraphQL from Scratch","stock":{"available":12}}]}}

Dört kitap stoğuyla, dördüncüsünün stoğu null, errors dizisinde de hatanın tam yeri: ["books", 3, "stock"]. Buna partial data denir ve REST'teki "ya hep ya hiç" yanıtlardan temel bir fark. Üç ders çıkıyor. Birincisi, başka bir sisteme giden ya da başarısız olabilecek field'ları nullable tasarla; non-null'ı yalnız gerçekten garanti edebildiğin alanlara ver. İkincisi, HTTP status yine 200. Hata yanıtın içinde ve client'ın errors'a bakması şart. Üçüncüsü, hata mesajı "Unexpected Execution Error". Hot Chocolate exception'ın kendisini client'a göstermiyor, iç ayrıntıları sızdırmamak için doğru bir varsayılan. Domain hatalarını client'a anlamlı biçimde döndürmenin yolunu bir sonraki yazıda mutation hatalarıyla birlikte ele alıyoruz.

September 2025 Spesifikasyonu ve GraphQL over HTTP

GraphQL spesifikasyonunun September 2025 sürümü, October 2021'den bu yana ilk resmî sürüm. Günlük kullanımı en çok etkileyen üç ekleme bu yazıdaki schema'da zaten var.

OneOf input object'leri. BookLookupInput @oneOf, "kitabı ya id ya ISBN ile bul, ikisini birden verme" demek. Eskiden bu ya iki ayrı field (bookById, bookByIsbn) ya da resolver içinde elle kontrolle çözülürdü. Artık doğrulama aşamasında:

{ book(by: { id: 1, isbn: "9780000000001" }) { title } }

400 application/graphql-response+json; charset=utf-8
{"errors":[{"message":"The OneOf Input Object `BookLookupInput` requires that exactly one field must be supplied and that field must not be `null`.","locations":[{"line":1,"column":12}],"path":["book"],"extensions":{"type":"BookLookupInput","specifiedBy":"https://spec.graphql.org/September2025/#sec-All-Variable-Usages-Are-Allowed"}}]}

Argument ve input field'larda @deprecated. Spesifikasyon artık @deprecated'i FIELD_DEFINITION ve ENUM_VALUE'nun yanında ARGUMENT_DEFINITION ve INPUT_FIELD_DEFINITION için de tanımlıyor. reviews'un eski limit argümanı introspection'da böyle görünüyor:

{"name":"reviews","args":[{"name":"first","isDeprecated":false,"deprecationReason":null},{"name":"limit","isDeprecated":true,"deprecationReason":"Use first."}]}

Executable document'larda description. Baştaki BookDetail sorgusunun üstündeki """Book detail page...""" satırı bir operation description; sorgunun ne için yazıldığını hem insanlara hem araçlara anlatıyor. Hot Chocolate 16 bunu kabul etti. Bunların yanında schema coordinate'ları (Book.reviews(limit:) gibi standart adresler) ve tam Unicode desteği de bu sürümle geldi. @defer ve @stream ise hâlâ spesifikasyonun dışında.

GraphQL'in HTTP üzerinden nasıl taşınacağı ayrı bir dokümanda tanımlanıyor ve bu doküman hâlâ taslak (Current Working Draft). Hot Chocolate 16'ya karşı denediklerim:

GET /graphql?query={ book(by: { id: 2 }) { title } }
HTTP/1.1 200 OK
Content-Type: application/graphql-response+json; charset=utf-8
{"data":{"book":{"title":"Practical Protobuf"}}}

GET /graphql?query=mutation { addReview(bookId: 2, rating: 5, text: "x") { id } }
HTTP/1.1 405 Method Not Allowed
Allow: POST
{"errors":[{"message":"The specified operation kind is not allowed."}]}

POST { book(by: { id: 2 } { title } }
HTTP/1.1 400 Bad Request
{"errors":[{"message":"Expected a `Name`-token, but found a `LeftBrace`-token.","locations":[{"line":1,"column":22}],"extensions":{"code":"HC0011"}}]}

POST { book(by: { id: 2 }) { title publisher } }
HTTP/1.1 400 Bad Request
{"errors":[{"message":"The field `publisher` does not exist on the type `Book`.","locations":[{"line":1,"column":31}],"extensions":{"type":"Book","field":"publisher","responseName":"publisher","specifiedBy":"https://spec.graphql.org/September2025/#sec-Field-Selections"}}]}

Query'ler GET ile de gönderilebilir; bu, HTTP cache'ini geri kazanmanın kapısı. Mutation GET ile gönderilemez; taslağın önerdiği gibi 405 ve Allow: POST. Response'un medya tipi application/graphql-response+json, ve bu tip istendiğinde parse ya da doğrulama hatası 4xx dönüyor. Aynı doğrulama hatalı isteği Accept: application/json ile gönderdiğimde ise yanıt 200 OK ve Content-Type: application/json oldu. Eski client'larla uyum için GraphQL hataları HTTP 200 içinde dönüyor. Taslağın güncel hâli doğrulama hataları için 422, partial data için de 294 kodunu öneriyor; Hot Chocolate 16 ise 400 ve 200 döndü. Taslak olmanın anlamı bu: status koduna değil, yanıttaki data ve errors'a bakan client yaz.

Üretimde Dikkat

Nullability'yi bir hata sınırı olarak tasarla. Başka bir service'e, cache'e ya da hesaplamaya bağlı field'lar nullable olsun. Non-null'ı ekranın onsuz anlamsız olduğu çekirdek alanlara sakla. Sonradan non-null'ı nullable yapmak client'ları kırar, çünkü onlar null kontrolü yazmamıştır.

Variable kullanmayan client'lara izin verme. Değeri sorgu metnine gömen client'lar her istekte farklı bir doküman gönderir; server'ın parse cache'i işe yaramaz, loglar okunmaz hâle gelir, persisted operation gibi güvenlik önlemleri de uygulanamaz.

Monitoring'i operation adına göre kur. Tek bir /graphql endpoint'inin p95'i hiçbir şey söylemez. Operation adı (BookDetail) ve resolver süreleri bazında ölç; anonim operation'ları reddetmeyi düşün.

Partial data'yı client'ta açıkça ele al. errors dolu ama data da dolu bir yanıt başarı mı, hata mı? Ekran bazında karar ver: stok rozeti gösterilemezse kitap sayfası yine açılsın, ödeme özeti eksikse açılmasın.

Sık Düşülen Tuzaklar

Her şeyi non-null yapmak. C#'tan gelen alışkanlıkla ! koymak, tek bir field'ın hatasını bütün yanıtı silen bir zincirleme null'a çevirir. Yukarıda beş kitap, bir hatayla sıfıra indi.

HTTP 200'ü başarı saymak. GraphQL hataları çoğu zaman 200 içinde gelir; errors'a bakmayan bir client, eksik veriyi sessizce gösterir.

Resolver'da N+1'i fark etmemek. Geliştirme ortamında üç kayıtla hızlı çalışan bir liste, production'da yüzlerce sorguya dönüşür. Resolver'ların veri erişimini loglamak bunu ilk günden gösterir.

REST endpoint'lerini birebir GraphQL field'larına çevirmek. getBookById, getAuthorById gibi düz bir liste, GraphQL'in ilişki gezintisini kullanmaz. Schema'yı ekranların gezdiği bir graph olarak tasarla.

Production'da introspection ve ayrıntılı hata mesajlarını açık bırakmak. Hot Chocolate 16 ikisini de varsayılan olarak kapalı getiriyor; bu ayarları "geliştirme kolaylığı" için açmadan önce iki kez düşün.

Küçük Bir Deneme

★☆☆ BookDetail sorgusuna author { name bio } ekle ve yanıt boyutunu REST'teki 492 byte ile karşılaştır. Hangi noktada GraphQL'in avantajı kaybolur?

★★☆ Review.text alanını C#'ta bir resolver ile nullable olmayan biçimde tanımla ve belirli bir yorum için exception fırlat. Null'ın nereye kadar yayıldığını önce reviews: [Review!]!, sonra reviews: [Review]! ile karşılaştır.

★★★ Product adında bir interface tanımla (title, price), Book ve yeni bir GiftCard tipi onu implement etsin. search'ü union yerine interface dönecek şekilde değiştir ve iki yaklaşımın client sorgularını nasıl etkilediğini yaz.

Kavramlar yerinde. Bir sonraki yazıda Hot Chocolate 16 ile Bookstore'un gerçek GraphQL API'sini kuruyoruz: schema yazma yaklaşımları arasında seçim, EF Core ile DbContext yönetimi, projection ve filtering'in ürettiği SQL ve typed error'lı mutation'lar.