How to call the Azure Cosmos DB REST API with C#
azure api

Authentication, request parameters and a few catches to not forget when calling the Cosmos DB API from C#.
June 23, 2022

A short one for me to remember. Cosmos DB, as many other Azure services, provides a REST API which allows developers to access data directly, without relying on the SDK or special tools. I wouldn't recommend using it regularly in a production application, but it can be handy for quick experiments and PoC validations.

In my case I wanted to generate a set of documents (10 000) to validate that my application performs as expected.

Authentication

As usual, authentication is the most complex part of the process. For the Cosmos API you need to build a signature, which is a compilation of several pieces of information. Fortunately, the format is well described, along with a helper method to generate it:

string GenerateMasterKeyAuthorizationSignature(HttpMethod verb, string resourceType, string resourceLink, string date, string key)
{
    var keyType = "master";
    var tokenVersion = "1.0";
    var payload = $"{verb.ToString().ToLowerInvariant()}\n{resourceType.ToLowerInvariant()}\n{resourceLink}\n{date.ToLowerInvariant()}\n\n";

    var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) };
    var hashPayload = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload));
    var signature = Convert.ToBase64String(hashPayload);
    var authSet = WebUtility.UrlEncode($"type={keyType}&ver={tokenVersion}&sig={signature}");

    return authSet;
}

Once the signature is generated, it is used in HTTP headers as Authorization:

// hc is HttpClient
hc.DefaultRequestHeaders.Add("Authorization", sig);

Calling the API

Actual API calls require a bunch of headers to be present:

// The same date must be used for the signature and request header.
var date = DateTime.UtcNow.ToString("R");

// Signature for document creation (POST).
var sig = GenerateMasterKeyAuthorizationSignature(HttpMethod.Post, "docs", $"dbs/mydatabase/colls/mycollection", date, "mycosmoskey");

var hc = new HttpClient();
hc.BaseAddress = new Uri("https://mycosmosaccount.documents.azure.com");
hc.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", "[mypartitionkeyvalue]");
hc.DefaultRequestHeaders.Add("x-ms-version", "2018-12-31");
hc.DefaultRequestHeaders.Add("x-ms-date", date);
hc.DefaultRequestHeaders.Add("Authorization", sig);

And then the request itself, aimed at the /docs resource:

// 'myObject' is a POCO created earlier
var content = new StringContent(JsonSerializer.Serialize(myObject), Encoding.UTF8, "application/json");
var resp = await hc.PostAsync($"/dbs/mydatabase/colls/mycollection/docs", content);

One thing I realized when running a long batch of requests is that the signature expires after 15 minutes. Quick and easy solution is to check for the HTTP response code and recreate the signature if needed:

if (!resp.IsSuccessStatusCode)
{
    if (resp.StatusCode == HttpStatusCode.Forbidden)
    {
        Console.WriteLine("Token expired, generating new one...");
        date = DateTime.UtcNow.ToString("R");
        sig = GenerateMasterKeyAuthorizationSignature(HttpMethod.Post, "docs", $"dbs/mydatabase/colls/mycollection", date, "mycosmoskey");
        hc.DefaultRequestHeaders.Remove("x-ms-date");
        hc.DefaultRequestHeaders.Remove("Authorization");

        hc.DefaultRequestHeaders.Add("x-ms-date", date);
        hc.DefaultRequestHeaders.Add("Authorization", sig);
    }
}

Reference

Found something inaccurate or plain wrong? Was this content helpful to you? Let me know!

šŸ“§ codez@deedx.cz