Call another GraphQL server
Source Trax.Api.GraphQL.Client/, Trax.Api.GraphQL.Client.Trax/, Trax.Api.GraphQL.Client.Typed/
Trax.Api.GraphQL.Client sends hand-written queries to a GraphQL endpoint and checks each one
against the server's schema before it goes out. It works in any .NET application; nothing in it
needs trains, the mediator or a database. Why it validates instead of generating code is in
How the GraphQL client checks queries.
1. Install the packages
dotnet add package Trax.Api.GraphQL.Client
dotnet add package Trax.Api.GraphQL.Client.Trax # optional: UseAssemblySchema, UseStartupValidation
dotnet add package Trax.Api.GraphQL.Client.Typed # optional: queries generated from a POCO2. Write a request
A request is a class implementing IGraphQLClientRequest<TResponse>: the query text, its
variables, and the type the result field is read into.
using Trax.Api.GraphQL.Client;
public sealed record PlayerProfile(string Id, string Name, int Level);
public sealed class GetPlayerRequest : IGraphQLClientRequest<PlayerProfile>
{
public required string Id { get; init; }
public string Query =>
"""
query GetPlayer($id: String!) {
player(id: $id) { id name level }
}
""";
public object Variables => new { id = Id };
}A document holds exactly one operation; fragments may come before or after it. Two other ways to write the same request:
- Put the query in a
.graphqlfile compiled as anEmbeddedResourceand derive from GraphQLResourceRequest, which also describes how the file is found. - Derive from
TypedRequest<TResponse>(in.Typed) and let the query be generated from the response type's properties. See Typed requests.
Nested envelopes (Path)
A Trax server puts a namespaced train's field under discover { <namespace> { ... } }. A typed
request reaches it with Path = "discover.<namespace>" on [GraphQLOperation]; see
GraphQLOperation properties.
3. Register the client and pick a schema source
using Trax.Api.GraphQL.Client;
builder
.Services.AddTraxGraphQLClient(new Uri("https://players.example.com/graphql"))
.UseFileSchema("schemas/players.graphql");Schema providers
Choose where the schema comes from:
| Call | Use it when |
|---|---|
none, or UseIntrospection() | The endpoint answers introspection from this caller. A Trax server does so only in Development, unless its host admits the caller through AllowIntrospection. |
UseFileSchema(path) | You check in an SDL snapshot of the server. The usual choice against a production Trax server. |
UseAssemblySchema(configure) (.Trax) | The server's HotChocolate configuration is in a .NET assembly you can reference. |
The calls are on TraxGraphQLClientBuilder.
4. Add authentication and timeouts
The client sends through a named HttpClient from IHttpClientFactory. Configure it through the
builder's HttpClientBuilder as you would any named client:
using Trax.Api.GraphQL.Client;
builder
.Services.AddTraxGraphQLClient(new Uri("https://players.example.com/graphql"))
.UseFileSchema("schemas/players.graphql")
.HttpClientBuilder.AddHttpMessageHandler<BearerTokenHandler>();BearerTokenHandler stands for your own DelegatingHandler, registered in the container. A
timeout goes on the same IHttpClientBuilder, with
ConfigureHttpClient(http => http.Timeout = TimeSpan.FromSeconds(30)).
Nothing is retried by default, because every request is a POST and a mutation is not safe to
repeat. If you add AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience,
call DisableForUnsafeHttpMethods() on its retry options, or add it only to clients that send
queries.
5. Validate requests at startup
With .Trax, chain UseStartupValidation and the host refuses to start when a request no longer
matches the schema:
using Trax.Api.GraphQL.Client;
using Trax.Api.GraphQL.Client.Trax;
builder
.Services.AddTraxGraphQLClient(new Uri("https://players.example.com/graphql"))
.UseFileSchema("schemas/players.graphql")
.UseStartupValidation(typeof(Program).Assembly);Without .Trax, call it yourself after the app is built, or from a test:
using Trax.Api.GraphQL.Client;
var app = builder.Build();
await app.Services.ValidateGraphQLClientAssembliesAsync(typeof(Program).Assembly);Which request types each call checks, and what it throws, is on ValidateGraphQLClientAssembliesAsync.
6. Run a request
Inject IGraphQLClientExecutor and pass it the request:
using Trax.Api.GraphQL.Client;
public sealed class PlayerLookup(IGraphQLClientExecutor graphql)
{
public Task<PlayerProfile> Find(string id, CancellationToken cancellationToken) =>
graphql.Run(new GetPlayerRequest { Id = id }, cancellationToken);
}A query the schema rejects throws GraphQLValidationException before any HTTP call. The other
exceptions Run throws are listed on
IGraphQLClientRequest.
7. Call several servers
Register each server under a key with AddKeyedTraxGraphQLClient, and mark each request with the
key of the client it belongs to, so each client validates only its own requests:
using Trax.Api.GraphQL.Client;
using Trax.Api.GraphQL.Client.Trax;
builder
.Services.AddKeyedTraxGraphQLClient("billing", new Uri("https://billing.example.com/graphql"))
.UseFileSchema("schemas/billing.graphql")
.UseStartupValidation(typeof(Invoice).Assembly);
builder
.Services.AddKeyedTraxGraphQLClient("shipping", new Uri("https://shipping.example.com/graphql"))
.UseFileSchema("schemas/shipping.graphql")
.UseStartupValidation(typeof(Invoice).Assembly);using Trax.Api.GraphQL.Client;
public sealed record Invoice(string Id, int TotalCents);
[GraphQLClient("billing")]
public sealed class GetInvoiceRequest : IGraphQLClientRequest<Invoice>
{
public required string Id { get; init; }
public string Query => "query GetInvoice($id: String!) { invoice(id: $id) { id totalCents } }";
public object Variables => new { id = Id };
}
public sealed class InvoiceLookup([FromKeyedServices("billing")] IGraphQLClientExecutor billing)
{
public Task<Invoice> Find(string id, CancellationToken cancellationToken) =>
billing.Run(new GetInvoiceRequest { Id = id }, cancellationToken);
}Each key gets its own configuration, HttpClient, schema provider and validator cache. A second
unkeyed AddTraxGraphQLClient call replaces the first rather than adding a client. Validation
refuses to start when a client finds no request marked for it, or when a request is marked with a
key no client is registered under; see
AddKeyedTraxGraphQLClient.
8. Catch response drift
To fail when the response and the response type disagree:
using Trax.Api.GraphQL.Client;
builder
.Services.AddTraxGraphQLClient(new Uri("https://players.example.com/graphql"))
.WithStrictness(ResponseStrictness.ThrowOnDrift);WarnOnDrift logs instead of throwing. What is compared is on
ResponseStrictness.
9. Point the client at a test server
In an end-to-end test, send through the HttpClient a WebApplicationFactory creates:
services
.AddTraxGraphQLClient(new Uri("http://localhost/trax/graphql"))
.ConfigureHttpClient(factory.CreateClient());Requests still go to the address passed to AddTraxGraphQLClient, whatever the HttpClient's
BaseAddress. The GraphQL Client sample does this in its E2E
suite, against two Trax servers with a keyed client each.
SDK Reference
AddTraxGraphQLClient | AddKeyedTraxGraphQLClient | TraxGraphQLClientBuilder | ValidateGraphQLClientAssembliesAsync | IGraphQLClientRequest | GraphQLResourceRequest | ResponseStrictness | Typed requests