Skip to content

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 ​

  • PostAsJsonAsync and ReadFromJsonAsync use the web defaults of System.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 whose errors array is not empty.
  • ListJobs follows pageInfo.endCursor until hasNextPage is false. The API returns at most 100 items per page.
  • externalId must be unique in your tenant. Running create twice with the same value returns a validation error in createJob.errors.

Connect machines, automate workflows, and build custom apps for print production.