CosmosDb+System.Text.Json - I take it all back

Knowing how to find answers from the incredibly useful, informal documentation that make up the collective knowledge on a subject turns out to be key.  Also, paying attention helps.

When you don't it sometimes results in an well deserved face palming moment when you say, "Why didn't I find/think/know/deduce that before I shot off my big mouth?"

And that's the case when you (in this case, me) write a snarky post about how something doesn't work when maybe if you'd had better access to the collective knowledge base, you'd have saved days of work and not a little embarrassment.  But then you'd be the Borg. Or is it a Borg?  I don't know... maybe both.

The solution came in the form of a comment in a GitHub repository where someone shared how they'd fixed the problem.  They used an option that I didn't even know existed on a method call.  And that's on me because I supplied every other possible parameter to that method except that one when I wrote my generic repository.

If you haven't read this post, don't.  It doesn't exist, and I don't know what you're talking about.  

If you did, forget I said all that and use the steps below to get CosmosDb and System.Text.Json working together without compromising.  And bonus - future proofing your app when System.Text.Json finally becomes the default serializer for CosmosDb.

Get a custom serializer.

You'll need a custom serializer to use instead of the Newtonsoft.Json one that's built in.  You can download that custom serializer here.  It's from the Microsoft.Azure.Cosmos.Samples repository so you can trust it.

Change your CosmosClient initialization code.

      JsonSerializerOptions options = new()
      {
        DefaultIgnoreCondition = JsonIgnoreCondition.Never,
        WriteIndented = true,
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
      };
      var cosmosClientOptions = new CosmosClientOptions()
      {
        ConnectionMode = ConnectionMode.Direct,
        HttpClientFactory = httpClientFactory.CreateClient,
        Serializer = new CosmosSystemTextJsonSerializer(options),
      };
      return new CosmosClient(connectionString, cosmosClientOptions);
Initializing the CosmosClient with the custom serializer you downloaded

As you can see, I'm setting PropertyNamingPolicy = JsonNamingPolicy.CamelCase so that my property names inside the document are all CamelCase.  If you're new to CosmosDb, remember that there is a case sensitive, required property with a name of id on every document.

Using a JsonNamingPolicy of CamelCase means I don't have to put [JsonPropertyName("id")] on my Id property or other Json attributes sprinkled around in my code.

What's important here is that this change does not affect the casing on LINQ queries.  So, if you stop reading here, you'll be unhappy.

Fix your LINQ queries.

If you're using LINQ for CosmosDb you might be writing something like this:

var query = container
    .GetItemLinqQueryable<Invitation>()
    .Where(i => i.InvitationId.IdValue == invitationId.IdValue);
A typical LINQ query.

Unfortunately, now that you're no longer using the default serializer, that LINQ statement generates the SQL below which has the wrong casing and yields the wrong result.

SELECT VALUE root FROM root WHERE 
(root["InvitationId"]["IdValue"] = "d8ca25ab-dd2f-434c-9e92-912f32aa923b")
SQL property names match class property names instead of document property names. Darn.

But you can fix that by changing your original query to this:

var query = container
    .GetItemLinqQueryable<Invitation>(
    linqSerializerOptions: new CosmosLinqSerializerOptions 
        { 
            PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
        })
    .Where(i => i.InvitationId.IdValue == invitationId.IdValue);
Passing serialization options to fix the casing in the query.

Now, the SQL generated looks like this:

SELECT VALUE root FROM root WHERE 
(root["invitationId"]["idValue"] = "d8ca25ab-dd2f-434c-9e92-912f32aa923b")
SQL property names now match casing of document property names. Yay.

As it turns out, for me anyway, I wrote a generic repository for my CosmosDb needs and I only needed to make the change in a single spot and voila, everything works as advertised.

Thanks to the people from Microsoft for responding in under 24 hours to a Tweet about this problem I was having and even more thanks for already solving it.  And a special thanks to the people at DevBetter for getting this out in the wind.