16

Is there a way to cast the Microsoft.Azure.Documents.Document object to my class type?

I've written an Azure Function class, with a CosmosDBTrigger. The trigger receives an array of Microsoft.Azure.Documents.Document. I like having that Document class so that I can access the meta data about the record itself, but I also would like to interact with my data from my class type in a static way.

I see the JSON representation of my data when I call ToString. Should I manually convert that JSON to my class type using Newtonsoft?

Ryan
  • 7,733
  • 10
  • 61
  • 106
  • [Cosmonaut](https://github.com/Elfocrash/Cosmonaut) can do that. The cosmosdb sdk can also that that by passing a `T` type in it's calls. You can find more about it here https://github.com/Azure/azure-documentdb-dotnet/tree/master/samples/code-samples – Nick Chapsas Jul 18 '18 at 21:16
  • @NickChapsas In this case, I'm not directly calling the cosmosdb sdk's fetch data methods. I'm using an azure function class, which is given the `Document`s. – Ryan Jul 18 '18 at 21:19

3 Answers3

14

If you need to map your Document to your POCO in the function then the easiest way to do that is what you suggested.

Call the document.Resource.ToString() method and use DeserializeObject from JSON.NET or the json library you prefer. JSON.NET is recommended however as Microsoft's CosmosDB libraries use it as well.

Your mapping call will look like this:

var yourPoco = JsonConvert.DeserializeObject<YourPocoType>(document.Resource.ToString())

Nick Chapsas
  • 6,872
  • 1
  • 20
  • 29
  • Note that `document.Resource` is already parsed as JObject. Proposed answer is inefficient as it converts from string-> JObject-> string -> JObject -> YourPoco on read. – Imre Pühvel Jul 20 '18 at 08:25
  • @ImrePühvel That only partially true. It is not a `JObject` but rather a class extending `JsonSerializable` which in return contains the data in a `JObject`. Internally the `JsonSerializable` does involve `JObject` parsing and manipulation but it is abstracted away from the consumer. The `ToString` method will internally do this `((object) this.propertyBag).ToString()` but the `propertyBag` which is the JObject you are talking about in inaccessible in any other way. – Nick Chapsas Jul 20 '18 at 08:45
  • The internal JObject instance in `propertyBag` is accessible via reflection. Should one find this hack acceptable for production use, depends. – Imre Pühvel Jul 20 '18 at 09:53
  • Are you gonna argue that using reflection should even be proposed in this case as a viable solution? – Nick Chapsas Jul 20 '18 at 09:59
  • Reflection on internals in never the first choice to make, but it MAY be viable if it is properly abstracted, documented and made sure fallback is readily available should things break. – Imre Pühvel Jul 20 '18 at 10:25
  • To make my initial concern about proposed solution clearer - double deserialization will make a negative performance impact. C# string manipulation is not the fastest to start with, and that avoidable `JObject`->`String`->`JObject` roundtrip creates both large strings (which may end up in LOH) and lots of smaller strings, not to mention a lot of JToken objects in heap + the computation overhead for JSON conversions. If this would become part of the core data access pipeline, then this adds up fast. It works, but is inefficient, beware. – Imre Pühvel Jul 20 '18 at 11:30
  • Document.Resource is no longer available ? – Kitwradr Apr 14 '20 at 11:13
  • Update: It's just `Document.ToString()` now. – Dave Loukola Feb 11 '22 at 16:42
9

While solution offered by Nick Chapsas works, I would like to offer a few better options.

Preferred solution - improve your model

First, if you are interested in the extra meta fields then you can always include the chosen properties into your data access model and they will be filled in. for example:

public class Model
{
    public String id { get; set; }
    public String _etag { get; set; }
    //etc.
}

Then you can use the existing API for deserializing thats explicit and familiar to all. For example:

var explicitResult = await client.ReadDocumentAsync<Model>(documentUri);
Model explicitModel = explicitResult.Document;

If you want the next layer model (ex: domain model) to NOT have those storage-specific meta fields then you need to transform to another model, but that is no longer a cosmosDB-level issue and there are plenty of generic mappers to convert between POCOs.

This is the IMHO cleanest and recommended way to handing data access in cosmosDB if you work on strongly typed document models.

Alternative: dynamic

Another trick is to use dynamic as the intermediate casting step. This is short and elegant in a way, but personally using dynamic always feels a bit dirty:

var documentResult = await client.ReadDocumentAsync(documentUri);
Model dynamicModel = (dynamic)documentResult.Resource;

Alternative: read JObject

Another alternative is to read the document as NewtonSoft's JObject. This would also include all the meta fields and you could cast it further yourself without all the extra hopping between string representations. Example:

var jObjectResult = await client.ReadDocumentAsync<JObject>(documentUri);
Model JObjectResult = jObjectResult.Document.ToObject<Model>();

Alternative: Document + JObject at the same time

Should you really-really want to avoid the document level meta fields in model AND still access them then you could use a little reflection trick to get the JObject from the Document instance:

var documentResult = await client.ReadDocumentAsync(documentUri);
Document documentModel = documentResult.Resource;

var propertyBagMember = documentResult.Resource.GetType()
    .GetField("propertyBag", BindingFlags.NonPublic| BindingFlags.Instance);
Model reflectionModel = ((JObject)propertyBagMember.GetValue(documentResult.Resource))
    .ToObject<Model>();

Beware that the reflection trick is relying on the internal implementation details and it is not subject to backwards compatibility guarantees by library authors.

Imre Pühvel
  • 4,468
  • 1
  • 34
  • 49
  • what is this client ? – Kitwradr Apr 14 '20 at 11:14
  • 1
    client = instance of "The service client that encapsulates the endpoint and credentials and connection policy used to access the Azure Cosmos DB service." [see here](https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.client.documentclient?view=azure-dotnet) MS does shuffle Azure API clients around a lot though. This one's for Microsoft.Azure.DocumentDB version 2.*. Old client syntax aside, the principles should still apply though for newer versions. – Imre Pühvel Apr 15 '20 at 07:52
  • Any luck with SDK3? it does not work on SDK3, which would throw exception right before result is returned. – 36Kr Nov 11 '20 at 23:33
  • @ImrePühvel the OP is talking about using `CosmosDBTrigger` to get the `Document`, so there is no `client` that can be used. Having a client would require ANOTHER I/O connection to CosmosDB which is almost guarenteed to be less performant than an in-memory call to JSON.net. – Dave Loukola Feb 11 '22 at 16:45
  • There is SOME cosmosDB-speaking client doing the mapping behind `CosmosDBTrigger` to produce the requested type, ie. IReadOnlyList, IReadOnlyList, etc. The proposed solutions are most likely usable and relevant regardless of the trigger wrapper. – Imre Pühvel Feb 15 '22 at 09:39
2

You can simply do a .ToString() in the Microsoft.Azure.Documents.Document class.

This class inherits from the Microsoft.Azure.Documents.JsonSerializable class that overrides the .ToString() method.

Here below is an example of deserializing the Document class to my Car.cs POCO using the new high-performant System.Text.Json Namespace:

Car car = JsonSerializer.Deserialize<Car>(document.ToString());

Dharman
  • 30,962
  • 25
  • 85
  • 135
John
  • 321
  • 2
  • 12