GraphQL Client
samples/GraphQLClient in Trax.Samples calls GraphQL
servers from .NET code with Trax.Api.GraphQL.Client. Each request is validated against the
server's schema before it is sent, so a query the server cannot answer fails in the caller, with no
HTTP round trip. Everything runs in memory: no database, no broker.
What it proves
| Feature | Where |
|---|---|
| One consumer, two Trax servers with different schemas, through two keyed clients in one container | Gateway/Program.cs, KeyedClientE2ETests |
A query written for one server refused by the other's client with GraphQLValidationException, before any HTTP call | KeyedClientE2ETests |
Typed requests reaching a Trax namespace with Path = "discover.<namespace>" | Gateway/Requests/KeyedRequests.cs |
The three ways to write a request: raw string (A), embedded .graphql resource (E), typed POCO (D) | Trax.Samples.GraphQLClient/Requests/ |
A client validating against the server's own schema configuration (UseAssemblySchema), so the two cannot drift | Trax.Samples.GraphQLClient/Program.cs |
Layout
samples/GraphQLClient/
├── Trax.Samples.GraphQLClient.InventoryServer/ Trax server: discover { inventory { getProduct } }
├── Trax.Samples.GraphQLClient.BillingServer/ Trax server: discover { billing { getInvoice } }
├── Trax.Samples.GraphQLClient.Gateway/ starts both, calls each through its keyed client
└── Trax.Samples.GraphQLClient/ a HotChocolate server and a client in modes A, E, DRun
From the Trax.Samples root:
dotnet run --project samples/GraphQLClient/Trax.Samples.GraphQLClient.Gateway
dotnet run --project samples/GraphQLClient/Trax.Samples.GraphQLClientThe inventory and billing servers can also run on their own in Development
(dotnet run --project samples/GraphQLClient/Trax.Samples.GraphQLClient.BillingServer), the inventory
server on 5313 and the billing server on 5314, serving GraphQL at /trax/graphql and health at
/trax/health.
Try it
The Gateway starts both servers on ports 5310 and 5311 and prints:
serverB (inventory) -> Mechanical Keyboard, 42 on hand
serverC (billing) -> invoice INV-1: 4999c (Paid)
serverB rejected a billing query (schema isolation holds)The modes sample serves its HotChocolate server on 5312 and ends with:
A == E (raw vs resource) : True
A ~ D (raw vs typed) : TrueIt exits with code 1 when either comparison is False.
How it works
The servers are ordinary Trax hosts
using Trax.Api.Extensions;
using Trax.Api.GraphQL.Extensions;
using Trax.Effect.Data.InMemory.Extensions;
using Trax.Effect.Extensions;
using Trax.Effect.Provider.Json.Extensions;
using Trax.Mediator.Extensions;
builder.Services.AddAuthorization();
builder.Services.AddTrax(trax =>
trax.AddEffects(effects => effects.UseInMemory().AddJson())
.AddMediator(mediator =>
mediator
.ScanAssemblies(typeof(IGetInvoiceTrain).Assembly)
.AllowMissingAuthorizationService()
)
);
builder.Services.AddTraxGraphQL(graphql => graphql.AllowIntrospection(_ => true));
builder.Services.AddHealthChecks().AddTraxHealthCheck();
var app = builder.Build();
app.UseTraxGraphQL();
app.MapHealthChecks("/trax/health");The one train is [TraxAllowAnonymous] and [TraxQuery(Namespace = "billing")], which serves it at
discover { billing { getInvoice(input: ...) } }. The client validates against the schema it fetches
by introspection, and a Trax server answers introspection only in Development unless the host passes
AllowIntrospection a predicate. These demo servers admit everyone; a production server admits only
the callers it trusts, or the client uses UseFileSchema or UseAssemblySchema instead. See
GraphQL Client: Schema Providers.
Two keyed clients in one container
using Trax.Api.GraphQL.Client;
var services = new ServiceCollection();
services.AddKeyedTraxGraphQLClient("serverB", new Uri("http://localhost:5310/trax/graphql"));
services.AddKeyedTraxGraphQLClient("serverC", new Uri("http://localhost:5311/trax/graphql"));
await using var provider = services.BuildServiceProvider();
var inventory = provider.GetRequiredKeyedService<IGraphQLClientExecutor>("serverB");
var billing = provider.GetRequiredKeyedService<IGraphQLClientExecutor>("serverC");
var product = await inventory.Run(new GetProductRequest { Input = new GetProductInput("SKU-1") });The key names the server; each executor validates against its own server's schema. Running
GetInvoiceRequest through the inventory executor throws GraphQLValidationException, because the
inventory schema has no getInvoice.
A typed request into a Trax namespace
using Trax.Api.GraphQL.Client.Typed;
[GraphQLType("GetProductOutput")]
public sealed record ProductView(string Sku, string Name, int QuantityOnHand);
[GraphQLOperation(OperationType.Query, Path = "discover.inventory", RootField = "getProduct")]
public sealed class GetProductRequest : TypedRequest<ProductView>
{
[GraphQLArgument("GetProductInput!", VariableName = "input")]
public required GetProductInput Input { get; init; }
}The record's properties are the selection set; Path wraps the root field in the discover and
namespace levels Trax generates, and the response is unwrapped along the same path. See
GraphQL Client: Nested envelopes.
Tests
dotnet test tests/Trax.Samples.GraphQLClient.E2EKeyedClientE2ETests boots both Trax servers with WebApplicationFactory and builds one container
with two keyed clients, each handed its server's in-process HttpClient through
.ConfigureHttpClient(factory.CreateClient()): each key queries its own server, unknown ids fall
back, and a query sent through the wrong key fails schema validation. The modes sample
(Trax.Samples.GraphQLClient) has no test of its own; it checks itself when run.
SDK Reference
AddTraxGraphQLClient | AddKeyedTraxGraphQLClient | Builder | IGraphQLClientRequest | AddTraxGraphQL