1

I am using Cosmosdb with .net CORE 2.2 and the Cosmosdb SQL SDK. By default cosmosdb will assign every documents 'Id' property as a Guid. But the Id alone is not enough to directly read the document, you must also know its partition. So I created a class named CosmosGuid that contains a Id property(Guid) and a PartitionKey property(string). The ToString() is overridden to call .ToString("N") on the Guid to remove the dashes and it appends the PartitionKey to the end. The problem im having is when I use the CosmosGuid in Linq, the generated SQL will contain a json version of CosmosGuid, I really need it to just be a string. I can call .ToString() and that will produce the desired result, but im afraid another developer will use my class in a Linq expression and it fail for no known reason. When I save the CosmosGuid I created a custom newtonsoft converter to call ToString() when it saves and to call .Parse(string) when it reads. When you compare two Guids in Linq the generated SQL comes out to a string, but when I compare two CosmosGuid it creates a json string of my class. How can I make my class act like a Guid?

I have already attempted to implement all the same Interfaces as the Guid. The closes I have came was implementing 'IEnumerable' and in the GetComparer() I returned:

new string[] { this.ToString() }.GetEnumerator();

The code produced was perfect, but it kept putting my string surrounded with brackets[].

here is an example:

SELECT VALUE root FROM root WHERE (root['id'] = ['9a9dbbd5f78143c48b16f780c7ceaa4011'])

This is the CosmosGuid class, I figure id post the full class since its not very large and it may be useful to some.

    public class CosmosGuid
    {
        // This is the unique Id of the entity
        public Guid Guid { get; set; }
        // This is the partition key where the entity lives
        public string PartitionKey { get; set; }
        // This is the unique Id of the Document that contains the entity
        public Guid? ParentGuid { get; set; }
        // This is the PartitionKey of the Document that contains the entity
        public string ParentPartitionKey { get; set; }

        /// <summary>
        /// Parses a CosmosGuid string into a new CosmosGuid
        /// </summary>
        /// <param name="cosmosGuid"></param>
        public CosmosGuid(string cosmosGuid)
        {
            ParentGuid = null;
            ParentPartitionKey = null;

            try
            {
                var parsed = cosmosGuid.Split('-');

                // We can accuratly parse the guid from the string by always grabing the first 32 characters.
                // The characters after the first 32 are the PartitionKey.
                // https://stackoverflow.com/a/4458925
                // Guid.NewGuid().ToString("N") => 32 characters (digits only, no dashes)

                Guid = Guid.Parse(parsed[0].Substring(0, 32));
                PartitionKey = parsed[0].Substring(32, parsed[0].Length - 32);

                if (parsed.Length == 2)
                {
                    ParentGuid = Guid.Parse(parsed[1].Substring(0, 32));
                    ParentPartitionKey = parsed[1].Substring(32, parsed[1].Length - 32);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("The Id of the document is not a properly formatted CosmosGuid.", ex);
            }
        }

        /// <summary>
        /// Generates a new Guid and appends the PartitionKey. This is used for Documents.
        /// </summary>
        /// <param name="partitionKey"></param>
        /// <returns></returns>
        public static CosmosGuid NewCosmosGuid(string partitionKey)
        {
            return new CosmosGuid($"{ShortenGuid(Guid.NewGuid())}{partitionKey}");
        }

        /// <summary>
        /// Generates a new Guid and appends the PartitionKey as well as the Parent Guid and Parent PartitionKey. This is used for Subdocuments.
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="partitionKey"></param>
        /// <returns></returns>
        public static CosmosGuid NewCosmosGuid(CosmosGuid parent, string partitionKey)
        {
            return new CosmosGuid($"{ShortenGuid(Guid.NewGuid())}{partitionKey}-{ShortenGuid(parent.Guid)}{parent.PartitionKey}");
        }

        /// <summary>
        /// Returns only the Parent CosmosGuid. If there is no parent the value returned will be null.
        /// </summary>
        public CosmosGuid Parent
        {
            get
            {
                if (ParentGuid != null && ParentPartitionKey != null)
                    return new CosmosGuid($"{ShortenGuid((Guid)ParentGuid)}{ParentPartitionKey}");
                else
                    return null;
            }
        }

        /// <summary>
        /// Parses a CosmosGuid string into a new CosmosGuid.
        /// </summary>
        /// <param name="cosmosGuid"></param>
        /// <returns></returns>
        public static CosmosGuid Parse(string cosmosGuid)
        {
            return new CosmosGuid(cosmosGuid);
        }

        /// <summary>
        /// Generates a CosmosGuid formatted string.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            if (ParentGuid == null)
                return $"{ShortenGuid(Guid)}{PartitionKey}";
            else
                return $"{ShortenGuid(Guid)}{PartitionKey}-{ShortenGuid((Guid)ParentGuid)}{ParentPartitionKey}";
        }

        /// <summary>
        /// Removes the dashes from a Guid
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        private static string ShortenGuid(Guid guid)
        {
            // Just remove dashes from the guid to shorten it some.
            // More can be done here if you wish but make sure the guid uniqueness isnt compromised.
            return guid.ToString("N");
        }

        public static bool operator ==(CosmosGuid obj1, CosmosGuid obj2)
        {
            return obj1?.ToString() == obj2?.ToString();
        }

        public static bool operator !=(CosmosGuid obj1, CosmosGuid obj2)
        {
            return obj1?.ToString() != obj2?.ToString();
        }
    }

If a developer where to use the CosmosGuid like so it would fail to work, because the SQL generated is a Json version of the class. (the Id is also a CosmosGuid):

var cosmosGuid = CosmosGuid.Parse("6bec688a0aca477c8175c09162b7a9b411");
var result = await Client.CreateDocumentQuery<MyClass>(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), options)
                                     .Where(x => x.Id == cosmosGuid)
                                     .AsDocumentQuery();

This is the sql generated

SELECT VALUE root FROM root WHERE (root['id'] = {'Guid':'6bec688a-0aca-477c-8175-c09162b7a9b4','PartitionKey':'11','ParentGuid':null,'ParentPartitionKey':null,'Parent':null})

Instead, the developer must call .ToString() everywhere in the code.

var cosmosGuid = CosmosGuid.Parse("6bec688a0aca477c8175c09162b7a9b411");
var result = await Client.CreateDocumentQuery<MyClass>(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), options)
                                     .Where(x => x.Id.ToString() == cosmosGuid.ToString())
                                     .AsDocumentQuery();

This is the Sql generated

SELECT VALUE root FROM root WHERE (root['id'] = '6bec688a0aca477c8175c09162b7a9b411')

If I remove the CosmosGuid and revert back to using just a Guid as the Id property the SQL generated by the Cosmosdb SDK works fine. How can I make my class act like a .net Guid when used in Linq?

  • Did you try overriding the `Equals` method and using that instead of `==`? – Hugo May 14 '19 at 17:11
  • Yes, it will get executed and I can compare the CosmosGuids there, but it does not make the SQL generated by the Cosmosdb SDK change.. – Capt. Quint May 14 '19 at 17:25
  • Can you share the SQL generated by your linq query? It may help to get a look at the output. – Hugo May 14 '19 at 17:26
  • Sure, ill update my question. – Capt. Quint May 14 '19 at 17:35
  • Hey I've been considering this a bit more. Can't test this here, but did you try playing with the JSON serializer settings on the `DocumentClient`? You might have to pass them explicitly rather than set globally. – w5l May 17 '19 at 09:44
  • I have already created a CosmosGuidConverter and have it set to be used by the JSON serializer. – Capt. Quint May 17 '19 at 13:39

1 Answers1

0

For LINQ to objects:

You can overload the == operator on your CosmosGuid class, see operator keyword.

Also, you could implement IEquatable<Guid> and use the .Equals() instead:

public class CosmosGuid : IEquatable<Guid>
{

  ....

  public bool Equals(Guid other) {
    return this.Guid == other;
  }
}
.Where(x => cosmosGuid.Equals(x.Id))
w5l
  • 5,341
  • 1
  • 25
  • 43