Schema (SDL)
The contract. Object, scalar, enum, input, interface and union types. ! means non-null.
Three operations
Query = read. Mutation = write (runs in order). Subscription = server push over WebSocket or SSE.
Resolvers
One function per field, called as the engine walks the query.
Errors
Usually HTTP 200 with an errors array. Alert on it, not the status.
.NET 10 How to implement it
| Need | Package | Notes |
| GraphQL server | HotChocolate.AspNetCore (16.6.x, MIT) | ChilliCream. Targets net10.0. Cost analysis is on by default |
| Source generator | HotChocolate.Types.Analyzers | [QueryType], [ObjectType<T>], [DataLoader] |
| EF Core paging, filters | HotChocolate.Data.EntityFramework | Cursor paging, filtering, sorting, projections |
| Persisted operations | HotChocolate.PersistedOperations.FileSystem / .Redis | Look up operations by hash |
| Subscriptions, many nodes | HotChocolate.Subscriptions.Redis | The default in-memory pub/sub works on one node only |
| Federation gateway | HotChocolate.Fusion.AspNetCore | Composes subgraphs (Composite Schemas spec) via the Nitro CLI or Aspire |
| Typed .NET client | StrawberryShake.Server | Generates a typed client from .graphql files |
| Alternative server | GraphQL.Server.Transports.AspNetCore (graphql-dotnet, 8.3.x) | Less common in new .NET projects |
[assembly: DataLoaderModule("BankDataLoaders")]
builder.Services.AddBankDataLoaders(); // generated from [DataLoader] methods
builder.Services
.AddGraphQLServer()
.AddTypes() // generated from [QueryType], [ObjectType<T>]
.ModifyCostOptions(o => { o.MaxFieldCost = 2_000; o.MaxTypeCost = 5_000; });
app.MapGraphQL(); // POST /graphql
[QueryType]
public static partial class AccountQueries
{
public static IQueryable<Account> GetAccounts(BankDb db) => db.Accounts.OrderBy(a => a.Id);
}
[ObjectType<Account>]
public static partial class AccountNode
{
public static Task<Customer?> GetOwnerAsync(
[Parent] Account account, ICustomerByIdDataLoader byId, CancellationToken ct)
=> byId.LoadAsync(account.CustomerId, ct); // batched: no N+1
}
internal static class CustomerDataLoaders
{
[DataLoader]
public static Task<Dictionary<int, Customer>> GetCustomerByIdAsync(
IReadOnlyList<int> ids, BankDb db, CancellationToken ct)
=> db.Customers.Where(c => ids.Contains(c.Id)).ToDictionaryAsync(c => c.Id, ct);
}
Keep every HotChocolate.* package on the same version. 16.6.x is the stable line; 16.7 builds are preview.
GraphQL is not "REST but better". It's a trade 🔄
✅ Clients get exactly the fields they need, in one round trip.
⚠️ But the client now writes the query, so the server must defend itself: cost limits, depth limits, page caps.
🐢 Nested fields cause N+1 queries. DataLoader batches them into one.
🧊 One POST endpoint means no easy HTTP caching. Persisted operations help.
🧩 Federation joins many teams' subgraphs into one supergraph, but only pays off at scale.
My rule: GraphQL for rich UIs and BFFs, REST for partners, gRPC or events inside. Where do you draw the line?
#GraphQL #APIDesign #SolutionArchitecture #DotNet