Monthly Archives: December 2021

Call the Azure DevOps REST API

In this post, I introduced the DevOps CLI. Here, I’m going to expand on that by interrogating the DevOps API, and generating a new work item in the board. A few years ago I did the same thing in TFS.


The first step here is to generate a personal access token. You can do this from the CLI, see here for details on how to do that.

From the UI, generating a personal access token is trivial; from your project, select Personal Access Tokens from the drop down menu:

In real life, the next screen is quite important, as you’ll want to scope down the access to the bare minimum. However, we’re just playing around, so for test purposes, we can grant full access:

You’ll then be given the token – take a copy of this:

The Code – Getting a list of projects in the organisation

The following code (heavily based on this link) should get a list of team projects within the organisation that you provide:

using System.Net.Http.Headers;

string personalaccesstoken = "abcdef";
string organisation = "devopsplayground1";

using HttpClient client = new HttpClient();

	new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
			string.Format("{0}:{1}", "", personalaccesstoken))));

using HttpResponseMessage response = await client.GetAsync(

string responseBody = await response.Content.ReadAsStringAsync();

personalaccesstoken is taken from the access token that you generated earlier, and the organisation is the name of your DevOps organisation; you can find it here if you’re unsure:

The Code – Creating a DevOps Work Item

Now that we can get a list of projects, we can pretty much do anything via the API; for example, if you wanted a list of work item types, you might use this:

    HttpResponseMessage responseWIT = await client.GetAsync(

    string responseBodyWIT = await responseWIT.Content.ReadAsStringAsync();

Updating or creating is a little different; let’s take creating a new work item. If I’m honest, the interface here doesn’t feel particularly RESTful, but nevertheless:

    var obj = new[] { new { from = (string?)null, op = "add", path = "/fields/System.Title", value = "pcmtest" } };
    string serialisedObj = System.Text.Json.JsonSerializer.Serialize(obj);
    var content = new StringContent(

    string type = "bug";
    HttpResponseMessage response = await client.PatchAsync(

    string responseBody = await response.Content.ReadAsStringAsync();

See here for the docs. There’s a few things to note here:

1. The type = “bug” comes from the types that we got above.
2. Note that the content that’s passed in to the Api call is an array – if you miss that and simply pass in a single object, you’ll get the error:

You must pass a valid patch document in the body of the request.

3. You will not see the above error unless, like me, you remove the EnsureSuccessStatusCode() call!
4. The API version matters, but it will give you a sensible error if you get it wrong.
5. Yes, this is a Patch call, even though you’re creating a new resource!

Here’s the new work item:


Running Docker Compose Interactively

I’ve recently been doing a fair bit of investigation into docker. My latest post was investigating the idea of setting up a sidercar pattern just using docker compose. In this post, I’m exploring the concept of launching an interactive shell using docker compose.

Why is this so hard?

Docker, and by extension, docker compose, are not really designed to run interactive tasks: they fare best left with ephemeral background tasks. However, in my particular use case, I needed to run a console app which accepted a keyboard input.

The system will let you create the console app as far as actually running, but once you issue a docker compose up, you’ll get something like the following error:

Unhandled exception. System.InvalidOperationException: Cannot read keys when either application does not have a console or when console input has been redirected. Try Console.Read.

How to run Interactively

In fact, there’s a few issues here, the first (as pointed out by this post) is that you need to tell docker compose to pass through the interactive mode to the container:

    build: .\My.App
    stdin_open: true
    tty: true    
      - "some-api"

stdin_open and tty translate to passing -it to the container. However, that doesn’t solve the problem entirely; if you issue a docker compose up you’ll still see the same issue.

The reason here is that there are (or can be) multiple containers, and so there’s no sensible way of knowing how to deal with that. In fact the solution is to simply tell docker compose which one to run; for example:

docker compose build --no-cache
docker compose run my-app

In the example above, that will also run the api some-api because we’ve declared a dependency. Note that build is optional; although while developing and debugging, I find it useful to execute both.

Docker error with .Net 6 runtime image

While trying to set-up a docker image for a .Net 6 console application, I found that, although it built fine, I got the an error when trying to run it. Let’s imagine that I used the following commands to build and run:

docker build -t pcm-exe
docker run pcm-exe

The result from the run command was the following:

It was not possible to find any compatible framework version
The framework ‘Microsoft.NETCore.App’, version ‘6.0.0’ (x64) was not found.
– The following frameworks were found:
6.0.0-rc.2.21480.5 at [/usr/share/dotnet/shared/Microsoft.NETCore.App]

You can resolve the problem by installing the specified framework and/or SDK.

The specified framework can be found at:

This had me stumped for a while, as I was under the impression that when images are updated, docker knows to go and download them – this is not the case. I discovered this by running an inspect on the runtime image from the dockerfile defined here:

FROM AS base

The inspect command:

docker inspect

This gave the following result:

“Env”: [

At this point there seemed to be two options – you can just remove the image – that would force a re-download; however, a quicker way is to pull the new version of the image:

docker pull 

After rebuilding the image, running a subsequent inspect shows that we’re now on the correct version:

          "Env": [