Appearance
Using the API from C# and .NET
This example uses only what ships with .NET: HttpClient, System.Net.Http.Json and System.Text.Json. It needs .NET 8 or later (the program uses C# 11 raw string literals), an API token, and the COCOCO_URL and COCOCO_TOKEN environment variables described in Calling the CoCoCo API.
The operations
graphql
query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}graphql
mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}The program
Create a project with dotnet new console -o Jobs and replace Program.cs with the program below. dotnet run lists every job; dotnet run -- create creates one.
csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;
const string ListJobsQuery = """
query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}
""";
const string CreateJobMutation = """
mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}
""";
var endpoint = Environment.GetEnvironmentVariable("COCOCO_URL")!.TrimEnd('/') + "/graphql";
var token = Environment.GetEnvironmentVariable("COCOCO_TOKEN")!;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Posts one operation and returns its data. Throws on an HTTP error or any top-level error.
async Task<T> GraphQL<T>(string query, object variables)
{
using var response = await http.PostAsJsonAsync(endpoint, new { query, variables });
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<T>>()
?? throw new InvalidOperationException("Empty response");
// Authentication, permission and query errors arrive here with HTTP 200.
if (result.Errors is { Count: > 0 })
{
throw new InvalidOperationException(string.Join("; ", result.Errors.Select(e => e.Message)));
}
return result.Data ?? throw new InvalidOperationException("Response has no data");
}
// Returns every job, fetching 100 per request.
async Task<List<Job>> ListJobs()
{
var jobs = new List<Job>();
string? after = null;
while (true)
{
var data = await GraphQL<ListJobsData>(ListJobsQuery, new { first = 100, after });
jobs.AddRange(data.ListJobs.Edges.Select(edge => edge.Node));
if (!data.ListJobs.PageInfo.HasNextPage)
{
return jobs;
}
after = data.ListJobs.PageInfo.EndCursor;
}
}
// Creates a job. Validation problems come back in the payload, not as top-level errors.
async Task<Job> CreateJob(string name, int quantity, string externalId)
{
var data = await GraphQL<CreateJobData>(CreateJobMutation, new { input = new { name, quantity, externalId } });
var payload = data.CreateJob;
if (payload.Errors.Count > 0)
{
throw new InvalidOperationException($"{payload.Errors[0].Path}: {payload.Errors[0].Message}");
}
return payload.Job ?? throw new InvalidOperationException("No job returned");
}
if (args.FirstOrDefault() == "create")
{
var job = await CreateJob("Business cards", 500, "ERP-10042");
Console.WriteLine($"Created {job.Id} ({job.Status})");
}
else
{
foreach (var job in await ListJobs())
{
Console.WriteLine($"{job.Id} {job.Status} {job.Quantity} {job.Name}");
}
}
record GraphQLResponse<T>(T? Data, List<GraphQLError>? Errors);
record GraphQLError(string Message);
// DueAt is ISO 8601 or null. The create mutation does not select Quantity or DueAt.
record Job(string Id, string Name, int Quantity, string Status, string? DueAt);
record PageInfo(bool HasNextPage, string? EndCursor);
record JobEdge(Job Node);
record JobConnection(int? TotalCount, PageInfo PageInfo, List<JobEdge> Edges);
record ListJobsData(JobConnection ListJobs);
record FieldError(string Path, string Message);
record CreateJobPayload(Job? Job, List<FieldError> Errors);
record CreateJobData(CreateJobPayload CreateJob);Notes
PostAsJsonAsyncandReadFromJsonAsyncuse the web defaults ofSystem.Text.Json: property names are written in camelCase and matched case-insensitively, so the anonymous objects and records map onto the GraphQL names.GraphQL<T>throws for both kinds of failure: an HTTP error status (EnsureSuccessStatusCode), and a response whoseerrorsarray is not empty.ListJobsfollowspageInfo.endCursoruntilhasNextPageisfalse. The API returns at most 100 items per page.externalIdmust be unique in your tenant. Runningcreatetwice with the same value returns a validation error increateJob.errors.