From 26920421c5cb99a3c26d0e5cb046170cb15e516c Mon Sep 17 00:00:00 2001 From: ewdlop Date: Wed, 27 Mar 2024 01:14:05 -0400 Subject: [PATCH] Fixing bugs, note for corners cases, and testing the codes(not unit tests) --- Program.cs | 220 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 83 deletions(-) diff --git a/Program.cs b/Program.cs index 1bdad6b..179ab77 100644 --- a/Program.cs +++ b/Program.cs @@ -8,19 +8,31 @@ //https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update-getting-started?tabs=dotnet //place holders -string connectionString = "AccountEndpoint=https://mycosmosaccount.documents.azure.com:443/;AccountKey=mykey==;"; -string databaseId = "myCosmosDBId"; +//should fetch the key from the Azure Key Vault +string connectionString = ""; +string databaseId = ""; + +dynamic sampleItem = new { id = "myId", name = "myName" }; // sample codes // still need more testing +// need to limit to 10 PatchOperations +// need to add more error handling +// need to not forget to add the "/" to the path PatchOperationList patchOperationList = new() { - PatchOperation.Add("age", 33), - PatchOperation.Add("address", new JObject + PatchOperation.Add("/age", 33), + PatchOperation.Add("/address", new JObject { { "city", "Seattle" }, { "state", "WA" }, - { "postalCode", "98052" } + { "postalCode", "98052" }, + { "test", new JObject + { + { "test1", "123" }, + { "test2", "456" } + } + } }), Enumerable.Range(0, 10).Select(i => new {}), //ignored 1, //ignored @@ -32,21 +44,35 @@ ("","",""),//ignored }; -PatchOperationList patchOperationList2 = []; +PatchOperationList patchOperationList2 = new() +{ + new JObject + { + { "city", "Seattle" }, + { "state", "WA" }, + { "postalCode", "98052" }, + { "test", new JObject + { + { "test1", "123" }, + { "test2", "456" } + } + } + } +}; PatchOperationList patchOperationList3 = new List(); -PatchOperationList patchOperationList4 = PatchOperation.Move("from", "to"); -PatchOperationList patchOperationList5 = [PatchOperation.Move("from", "to"), PatchOperation.Move("here", "there")]; -PatchOperationList patchOperationList6 = new PatchOperationList { PatchOperation.Move("from", "to"), PatchOperation.Move("here", "there") }; -PatchOperationList patchOperationList7 = new PatchOperationList(new List { PatchOperation.Move("from", "to"), PatchOperation.Move("here", "there") }); +PatchOperationList patchOperationList4 = PatchOperation.Move("/from", "/to"); +PatchOperationList patchOperationList5 = [PatchOperation.Move("/from", "/to"), PatchOperation.Move("/here", "/there")]; +PatchOperationList patchOperationList6 = new PatchOperationList { PatchOperation.Move("/from", "/to"), PatchOperation.Move("/here", "/there") }; +PatchOperationList patchOperationList7 = new PatchOperationList(new List { PatchOperation.Move("/from", "/to"), PatchOperation.Move("/here", "/there") }); List patchOperations = patchOperationList; -patchOperations.Add(PatchOperation.Move("from", "to"), PatchOperation.Move("here", "there")); +patchOperationList3.Add(PatchOperation.Move("/from", "/to"), PatchOperation.Move("/here", "/there")); IList patchOperations2 = new PatchOperationList(); IReadOnlyList patchOperations3 = patchOperationList; PatchOperationList patchOperationList8 = new PatchOperationListBuilder() - .With(PatchOperation.Move("from", "to")) + .With(PatchOperation.Move("/from", "/to")) .WithAdd("age", 70) .WithAdd("address", new JObject { @@ -77,18 +103,23 @@ CosmosClient client = new(connectionString); Database database = client.GetDatabase(databaseId); string partitionKeyPath = "/myPartitionKey"; - ContainerResponse response = await database.CreateContainerIfNotExistsAsync("myContainer", partitionKeyPath); - if (response.StatusCode == HttpStatusCode.Created) + ContainerResponse response = await database.CreateContainerIfNotExistsAsync("myContainer3", partitionKeyPath); + if (response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.OK) { + Console.WriteLine(response.StatusCode == HttpStatusCode.Created ? "Container created" : "Container already exists"); Container container = response.Container; - dynamic item = new { id = "myId", name = "myName" }; - string myPartitionKey = $"{partitionKeyPath}/1"; - PartitionKey partitionKey = new PartitionKey(myPartitionKey); - ItemResponse itemResponse = await container.CreateItemAsync(item, new PartitionKey(myPartitionKey)); + Guid guid = Guid.NewGuid(); + string itemPartitionKey = "itemPartitionKey"; + dynamic item = new { id = guid.ToString(), name = "myName", myPartitionKey = itemPartitionKey }; + string myPartitionKey = $"{itemPartitionKey}"; + + dynamic itemResponse = await container.CreateItemAsync(item); Console.WriteLine($"Created item in database with id: {itemResponse.Resource.id}"); //Partial update - ItemResponse patchResponse = await container.PatchItemAsync(item.id, partitionKey, patchOperationList); + PartitionKey partitionKey = new PartitionKey(myPartitionKey); + ItemResponse patchResponse = await container.PatchItemAsync(item.id, partitionKey, patchOperations); + if(patchResponse.StatusCode == HttpStatusCode.OK) { Console.WriteLine($"Patched item in database with id: {patchResponse.Resource.id}"); @@ -97,10 +128,17 @@ { Console.WriteLine($"Failed to patch item in database with id: {patchResponse.Resource.id}"); } - } - else if (response.StatusCode == HttpStatusCode.OK) - { - Console.WriteLine("Failed to create container or container already exists"); + + ItemResponse patchResponse2 = await container.PatchItemAsync(item.id, partitionKey, patchOperationList2); + + if (patchResponse2.StatusCode == HttpStatusCode.OK) + { + Console.WriteLine($"Patched item in database with id: {patchResponse2.Resource.id}"); + } + else + { + Console.WriteLine($"Failed to patch item in database with id: {patchResponse2.Resource.id}"); + } } } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound || @@ -117,9 +155,13 @@ Console.WriteLine($"Exception: {ex}"); } -//Not done yet namespace CosmosDBPartialUpdateTypeConverter { + /// + ///Not done yet + ///Need to limit to 10 PatchOperations and more testing + ///https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update-faq + /// public class PatchOperationList : IList, IReadOnlyList { private readonly List _patchOperations = []; @@ -161,10 +203,10 @@ PatchOperation IList.this[int index] public void Add(PatchOperation item) => _patchOperations.Add(item); - public void Add(JObject value) + public void Add(JObject jObject) { - ArgumentNullException.ThrowIfNull(value, nameof(value)); - _patchOperations.Add(value); + ArgumentNullException.ThrowIfNull(jObject, nameof(jObject)); + _patchOperations.Add(jObject); } public void Add(T entity) @@ -232,8 +274,13 @@ public void AddSet(params T[] entities) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void ForEach(Action action) => _patchOperations.ForEach(action); + + public IReadOnlyList AsReadOnly() => _patchOperations.AsReadOnly(); } + /// + /// Need more work + /// public static class PatchOperationExtension { public static List Add(this List patchOperations, params PatchOperation[] entities) @@ -244,28 +291,25 @@ public static List Add(this List patchOperations public static List Add(this List patchOperations, string path, object? value) { - patchOperations.Add(PatchOperation.Add(path, value)); + patchOperations.Add(PatchOperation.Add(BuildPath(path), value)); return patchOperations; } public static List Add(this List patchOperations, string path, T? value) { - patchOperations.Add(PatchOperation.Add(path, value)); + patchOperations.Add(PatchOperation.Add(BuildPath(path), value)); return patchOperations; } - //TODO - public static List Add(this List patchOperations, JObject value) + public static List Add(this List patchOperations, JObject jObject) { - //foreach (var property in value.Properties()) - //{ - // var propertyValue = property.Value.Type == JTokenType.Object || property.Value.Type == JTokenType.Array - // ? property.Value.ToString() // For nested objects or arrays, convert to string - // : (object)property.Value.ToObject(typeof(object)); // Convert simple values to their native types - - // patchOperations.Add(PatchOperation.Add(property.Path, propertyValue)); - //} - + ArgumentNullException.ThrowIfNull(jObject, nameof(jObject)); + foreach (var property in jObject.Properties()) + { + string key = property.Name; + JToken value = property.Value; + patchOperations.Add(PatchOperation.Add(BuildPath(key), value)); + } return patchOperations; } @@ -275,7 +319,7 @@ public static List Add(this List patchOperati patchOperations.AddRange(entity switch { string => Enumerable.Empty(), - _ => typeof(T).GetProperties().Select(property => PatchOperation.Add(property.Name, property.GetValue(entity))) + _ => typeof(T).GetProperties().Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(entity))) }); return patchOperations; } @@ -284,7 +328,7 @@ public static List Add(this List patchOperati where T : class { patchOperations.AddRange(entities.Where(entity => entity is not string) - .SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Add(property.Name, property.GetValue(entity))))); + .SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(entity))))); return patchOperations; } @@ -293,8 +337,8 @@ public static List Add(this List patchOperati { patchOperations.AddRange(entities switch { - [string item1, string item2] => [PatchOperation.Add(item1, item2)], - _ => entities.Where(entity => entity is not string).SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Add(property.Name, property.GetValue(entity)))) + [string item1, string item2] => [PatchOperation.Add(BuildPath(item1), item2)], + _ => entities.Where(entity => entity is not string).SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(entity)))) }); return patchOperations; } @@ -304,11 +348,11 @@ public static List Add(this List patchOperations ArgumentNullException.ThrowIfNull(value, nameof(value)); patchOperations.AddRange(value switch { - (string item1, object item2) => [PatchOperation.Add(item1, item2)], - (object item1, object item2) => [ PatchOperation.Add(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Add(BuildPath(item1), item2)], + (object item1, object item2) => [ PatchOperation.Add(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties().Where(propertyInfoFilter is not null ? propertyInfoFilter : _ => true) - .Select(property => PatchOperation.Add(property.Name, property.GetValue(value))) + .Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(value))) }); return patchOperations; } @@ -317,11 +361,11 @@ public static List Add(this List patchOperations { patchOperations.AddRange(values.SelectMany(value => value switch { - (string item1, object item2) => [PatchOperation.Add(item1, item2)], - (object item1, object item2) => [PatchOperation.Add(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Add(BuildPath(item1), item2)], + (object item1, object item2) => [PatchOperation.Add(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties() - .Select(property => PatchOperation.Add(property.Name, property.GetValue(value))) + .Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(value))) })); return patchOperations; } @@ -330,18 +374,21 @@ public static List Add(this List patchOperations { patchOperations.AddRange(values.SelectMany(value => value switch { - (string item1, object item2) => [PatchOperation.Add(item1, item2)], - (object item1, object item2) => [PatchOperation.Add(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Add(BuildPath(item1), item2)], + (object item1, object item2) => [PatchOperation.Add(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties() - .Select(property => PatchOperation.Add(property.Name, property.GetValue(value))) + .Select(property => PatchOperation.Add(BuildPath(property.Name), property.GetValue(value))) })); return patchOperations; } - + + //For Appending to an json array + //the path must exist first + //need more testing for the corner cases public static List AddAppend(this List patchOperations, string path, object? value) { - patchOperations.Add(PatchOperation.Add($"{path}/`", value)); + patchOperations.Add(PatchOperation.Add($"/{path}/`", value)); return patchOperations; } @@ -377,28 +424,13 @@ public static List AddReplace(this List patchOpe public static List AddSet(this List patchOperations, string path, object? value) { - patchOperations.Add(PatchOperation.Set(path, value)); + patchOperations.Add(PatchOperation.Set(BuildPath(path), value)); return patchOperations; } public static List AddSet(this List patchOperations, string path, T? value) { - patchOperations.Add(PatchOperation.Set(path, value)); - return patchOperations; - } - - //TODO - public static List AddSet(this List patchOperations, JObject value) - { - //foreach (var property in value.Properties()) - //{ - // var propertyValue = property.Value.Type == JTokenType.Object || property.Value.Type == JTokenType.Array - // ? property.Value.ToString() // For nested objects or arrays, convert to string - // : (object)property.Value.ToObject(typeof(object)); // Convert simple values to their native types - - // patchOperations.Add(PatchOperation.Add(property.Path, propertyValue)); - //} - + patchOperations.Add(PatchOperation.Set(BuildPath(path), value)); return patchOperations; } @@ -408,16 +440,28 @@ public static List AddSet(this List patchOper patchOperations.AddRange(entity switch { string => Enumerable.Empty(), - _ => typeof(T).GetProperties().Select(property => PatchOperation.Set(property.Name, property.GetValue(entity))) + _ => typeof(T).GetProperties().Select(property => PatchOperation.Set(BuildPath(property.Name), property.GetValue(entity))) }); return patchOperations; } + public static List AddSet(this List patchOperations, JObject jObject) + { + ArgumentNullException.ThrowIfNull(jObject, nameof(jObject)); + foreach (var property in jObject.Properties()) + { + string key = property.Name; + JToken value = property.Value; + patchOperations.Add(PatchOperation.Set(BuildPath(key), value)); + } + return patchOperations; + } + public static List AddSet(this List patchOperations, IEnumerable entities) where T : class { patchOperations.AddRange(entities.Where(entity => entity is not string) - .SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Set(property.Name, property.GetValue(entity))))); + .SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Set(BuildPath(property.Name), property.GetValue(entity))))); return patchOperations; } @@ -426,8 +470,8 @@ public static List AddSet(this List patchOper { patchOperations.AddRange(entities switch { - [string item1, string item2] => [PatchOperation.Set(item1, item2)], - _ => entities.Where(entity => entity is not string).SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Set(property.Name, property.GetValue(entity)))) + [string item1, string item2] => [PatchOperation.Set(BuildPath(item1), item2)], + _ => entities.Where(entity => entity is not string).SelectMany(entity => entity.GetType().GetProperties().Select(property => PatchOperation.Set(BuildPath(property.Name), property.GetValue(entity)))) }); return patchOperations; } @@ -437,8 +481,8 @@ public static List AddSet(this List patchOperati ArgumentNullException.ThrowIfNull(value, nameof(value)); patchOperations.AddRange(value switch { - (string item1, object item2) => [PatchOperation.Set(item1, item2)], - (object item1, object item2) => [PatchOperation.Set(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Set(BuildPath(item1), item2)], + (object item1, object item2) => [PatchOperation.Set(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties().Where(propertyInfoFilter is not null ? propertyInfoFilter : _ => true) .Select(property => PatchOperation.Set(property.Name, property.GetValue(value))) @@ -450,8 +494,8 @@ public static List AddSet(this List patchOperati { patchOperations.AddRange(values.SelectMany(value => value switch { - (string item1, object item2) => [PatchOperation.Set(item1, item2)], - (object item1, object item2) => [PatchOperation.Set(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Set(BuildPath(item1), item2)], + (object item1, object item2) => [PatchOperation.Set(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties() .Select(property => PatchOperation.Set(property.Name, property.GetValue(value))) @@ -463,16 +507,26 @@ public static List AddSet(this List patchOperati { patchOperations.AddRange(values.SelectMany(value => value switch { - (string item1, object item2) => [PatchOperation.Set(item1, item2)], - (object item1, object item2) => [PatchOperation.Set(item1.ToString(), item2)], + (string item1, object item2) => [PatchOperation.Set(BuildPath(item1), item2)], + (object item1, object item2) => [PatchOperation.Set(BuildPath(item1.ToString()), item2)], //need to check null or do something int or long or float or double or decimal or string or bool or null => Enumerable.Empty(), _ => value.GetType().GetProperties() - .Select(property => PatchOperation.Set(property.Name, property.GetValue(value))) + .Select(property => PatchOperation.Set(BuildPath(property.Name), property.GetValue(value))) })); return patchOperations; } + + //Todo + //Should use regex to check if string is of the form ^\/[A-Za-z0-9]+$ + //or of the form ^\/[A-Za-z0-9]+\/[A-Za-z0-9]+$, recursively for more than 2 level + //for path such as /address/city + public static string BuildPath(string path) + { + return path.StartsWith('/') ? path : $"/{path}"; + } } + //Possibly need more work public class PatchOperationListBuilder(List _patchOperations) { public PatchOperationListBuilder() : this(new List()) { }