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

FeatureWhere
One consumer, two Trax servers with different schemas, through two keyed clients in one containerGateway/Program.cs, KeyedClientE2ETests
A query written for one server refused by the other's client with GraphQLValidationException, before any HTTP callKeyedClientE2ETests
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 driftTrax.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, D

Run

From the Trax.Samples root:

dotnet run --project samples/GraphQLClient/Trax.Samples.GraphQLClient.Gateway
dotnet run --project samples/GraphQLClient/Trax.Samples.GraphQLClient

The 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)     : True

It 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.E2E

KeyedClientE2ETests 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