GraphQL to
C#.
Turn a GraphQL SDL schema into a modern C# client. Records with required and init, nullable reference types straight from the schema's !, a [JsonPolymorphic] interface for every union. Typed query, mutation, and subscription clients over HttpClient — subscriptions surface as IAsyncEnumerable<T> over a graphql-transport-ws socket. No signup, no install.
scalar DateTime
enum Currency {
EUR
USD
}
enum PaymentStatus {
PENDING
SETTLED
FAILED
}
type CardPayment {
last4: String!
brand: String!
}
type BankPayment {
iban: String!
}
union PaymentMethod = CardPayment | BankPayment
type Payment {
id: ID!
customerId: String!
amount: Int!
currency: Currency!
status: PaymentStatus!
description: String
method: PaymentMethod
createdAt: DateTime!
}// Models/Payment.cs
using System;
using System.Text.Json.Serialization;
namespace Example.Payments.Models;
public record Payment
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("customerId")]
public required string CustomerId { get; init; }
[JsonPropertyName("amount")]
public required int Amount { get; init; }
[JsonPropertyName("currency")]
public required Currency Currency { get; init; }
[JsonPropertyName("status")]
public required PaymentStatus Status { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("method")]
public PaymentMethod? Method { get; init; }
[JsonPropertyName("createdAt")]
public required DateTime CreatedAt { get; init; }
}Idiomatic C#. Not a generated-sources folder.
Below: types emitted from a small payments schema with a union, a nullable field, and an enum. Every type is a record with init properties; required covers every non-null SDL field; the union becomes a [JsonPolymorphic] interface keyed on __typename, so a plain switch matches it.
// Models/Payment.cs
public record Payment
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("method")]
public PaymentMethod? Method { get; init; }
[JsonPropertyName("createdAt")]
public required DateTime CreatedAt { get; init; }
// ...
}
// Models/Currency.cs
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Currency
{
EUR,
USD
}
// Models/PaymentMethod.cs
[JsonPolymorphic(TypeDiscriminatorPropertyName = "__typename")]
[JsonDerivedType(typeof(CardPayment), "CardPayment")]
[JsonDerivedType(typeof(BankPayment), "BankPayment")]
public interface PaymentMethod { }
// Match the union with a plain switch — System.Text.Json
// reads __typename and hands you the concrete record:
string Label(Payment p) => p.Method switch
{
CardPayment c => $"card {c.Last4}",
BankPayment b => $"bank {b.Iban}",
null => "no method",
_ => throw new InvalidOperationException(),
};Grounded in how the schema reads.
Each claim is something you can verify by running the converter on your own .graphql schema. Click a card to see the SDL fragment that triggered it and the C# that came out.
type Payment {
id: ID!
description: String
method: PaymentMethod
createdAt: DateTime!
# ...
}public record Payment
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("method")]
public PaymentMethod? Method { get; init; }
[JsonPropertyName("createdAt")]
public required DateTime CreatedAt { get; init; }
// ...
}
// - record, not a class with setters
// - 'String!' -> required string; 'String' -> string?
// - a nullable union field becomes PaymentMethod?
// - DateTime via --custom-scalar DateTime=System.DateTime
// - [JsonPropertyName] pins the camelCase wire nameTyped clients, not a raw transport.
Operations split into QueryClient, MutationClient, and SubscriptionClient, all extending a thin BaseHttpClient that owns the HttpClient and base URL. Queries and mutations POST a GraphQL document — with the full selection set generated inline — to /graphql; ExecuteGraphQLAsync reads data.<field> and surfaces an errors[] array as an HttpRequestException. Subscriptions return IAsyncEnumerable<T> over a graphql-transport-ws WebSocket that the base client derives from the base URL. System.Text.Json handles both sides.
// Client/QueryClient.cs
using System.Net.Http;
using System.Text.Json;
using Example.Payments.Models;
namespace Example.Payments.Client;
public class QueryClient : BaseHttpClient
{
public QueryClient(HttpClient httpClient, string baseUrl, JsonSerializerOptions? jsonOptions)
: base(httpClient, baseUrl, jsonOptions) { }
public async Task<Example.Payments.Models.Payment?> PaymentAsync(
string id, CancellationToken cancellationToken = default)
{
var variables = new Dictionary<string, object?> { { "id", id } };
return await ExecuteGraphQLAsync<Example.Payments.Models.Payment>(
"query Payment($id: ID!) { payment(id: $id) { ... } }", // full selection set generated inline
"Payment", variables, "payment", cancellationToken).ConfigureAwait(false);
}
}
// Client/SubscriptionClient.cs
public class SubscriptionClient : BaseHttpClient
{
public async IAsyncEnumerable<Example.Payments.Models.Payment> PaymentSettledAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var item in SubscribeAsync<Example.Payments.Models.Payment>(
"subscription PaymentSettled { paymentSettled { ... } }",
"PaymentSettled", null, "paymentSettled", cancellationToken).ConfigureAwait(false))
{
yield return item;
}
}
}
// Client/BaseHttpClient.cs (shared)
public abstract class BaseHttpClient
{
protected readonly HttpClient _httpClient;
protected readonly string _baseUrl;
protected readonly JsonSerializerOptions? _jsonOptions;
protected readonly GraphQLWebSocketConnection _ws;
public BaseHttpClient(HttpClient httpClient, string baseUrl, JsonSerializerOptions? jsonOptions = null)
{
_httpClient = httpClient;
_baseUrl = baseUrl.TrimEnd('/');
_jsonOptions = jsonOptions;
// http -> ws, https -> wss; subscriptions hit <baseUrl>/graphql
var wsBase = _baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
? "wss://" + _baseUrl.Substring("https://".Length)
: "ws://" + _baseUrl.Substring("http://".Length);
_ws = new GraphQLWebSocketConnection(new Uri(wsBase + "/graphql"), _jsonOptions);
}
protected async Task<T?> ExecuteGraphQLAsync<T>(
string query, string operationName,
Dictionary<string, object?>? variables, string fieldName,
CancellationToken cancellationToken)
{
var graphqlBody = new Dictionary<string, object?>
{
{ "query", query },
{ "operationName", operationName },
};
if (variables != null) graphqlBody["variables"] = variables;
using var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/graphql");
request.Content = new StringContent(
JsonSerializer.Serialize(graphqlBody, _jsonOptions), Encoding.UTF8, "application/json");
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// Reads data.<fieldName>; an errors[] array surfaces as HttpRequestException.
return await ReadGraphQLFieldAsync<T>(response, fieldName, cancellationToken).ConfigureAwait(false);
}
// Subscriptions: one ClientWebSocket (graphql-transport-ws) multiplexes every
// active subscription; each yields its 'next' frames as IAsyncEnumerable<T>.
protected async IAsyncEnumerable<T> SubscribeAsync<T>(
string query, string operationName,
Dictionary<string, object?>? variables, string fieldName,
[EnumeratorCancellation] CancellationToken cancellationToken)
{ /* connection_init -> subscribe -> read next/error/complete */ }
}Or ask Claude to do it.
The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a .graphql schema and it produces the C# client as a diff — free, no token, no leaving the chat.
// claude-desktop config · .mcp.json
{
"mcpServers": {
"metaengine": {
"command": "npx",
"args": ["-y", "@metaengine/mcp-server"]
}
}
}
// Then in Claude:
// "Load schema.graphql and generate a C# HttpClient
// in namespace Acme.Payments."Things people ask.
The questions we keep getting about GraphQL → C#. If yours is not here, run the converter — the answer is usually in the output.
.cs files: one record per object and input type, one enum per enum, a [JsonPolymorphic] interface plus member records per union, and a typed client per operation root — QueryClient, MutationClient, SubscriptionClient. Plus a shared BaseHttpClient with the GraphQL-over-HTTP POST helper and the subscription WebSocket. System.Text.Json only — no extra runtime package.Same engine, other stacks.
Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.
Try it on your own schema.
The converter runs in the browser. Your schema never leaves the page.