From e1d07fa1b8fd43f8a107cf855148c2e395bfd8a5 Mon Sep 17 00:00:00 2001 From: Xiangzhi Sheng Date: Wed, 23 Nov 2016 14:44:20 +0800 Subject: [PATCH] Add support to get/set value of tag/property at any level In previous implementation, there is a limitation in the Get/Set method of TwinExtension: only the value at top level could be get/set. In other words, Twin.Tags.Get("Location.City") is not supported (it will cause exception raised in SDK). In this code change, we are going to enable get/set at any level. --- Common/Extensions/TwinCollectionExtension.cs | 194 +++++++++++++++++- Common/Extensions/TwinExtension.cs | 4 +- .../Common/TwinCollectionExtensionTests.cs | 53 +++++ 3 files changed, 245 insertions(+), 6 deletions(-) diff --git a/Common/Extensions/TwinCollectionExtension.cs b/Common/Extensions/TwinCollectionExtension.cs index fe775024..501cc6de 100644 --- a/Common/Extensions/TwinCollectionExtension.cs +++ b/Common/Extensions/TwinCollectionExtension.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extensions @@ -21,6 +22,11 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extension /// Enumerator returns the flat name and value static public IEnumerable> AsEnumerableFlatten(this TwinCollection collection, string prefix = "") { + if (collection == null) + { + throw new ArgumentNullException(); + } + foreach (KeyValuePair pair in collection) { if (pair.Value is TwinCollection) @@ -47,12 +53,12 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extension LastUpdated = (pair.Value as TwinCollectionValue)?.GetLastUpdated() }); } -#if DEBUG else { +#if DEBUG throw new ApplicationException($"Unexpected TwinCollection item type: {pair.Value.GetType().FullName} @ {prefix}{pair.Key}"); - } #endif + } } } @@ -76,12 +82,192 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extension LastUpdated = (child.Value as TwinCollectionValue)?.GetLastUpdated() }); } -#if DEBUG else { +#if DEBUG throw new ApplicationException($"Unexpected TwinCollection item JTokenType: {child.Value.Type} @ {prefix}{child.Name}"); - } #endif + } + } + } + + /// + /// Get value of given tag or property specified by the flat name + /// + /// Twin.Tag, Twin.Properties.Desired or Twin.Properties.Reported + /// The flat name with prefix such as 'tags.' and so on + /// The value in dynamic, or null in case error + static public dynamic Get(this TwinCollection collection, string flatName) + { + if (collection == null || string.IsNullOrWhiteSpace(flatName)) + { + throw new ArgumentNullException(); + } + + return Get(collection, flatName.Split('.')); + } + + static private dynamic Get(this TwinCollection collection, IEnumerable names) + { + var name = names.First(); + + // Pick node on current level + if (!collection.Contains(name)) + { + // No desired node found. Return null as error + return null; + } + var child = collection[name]; + + if (names.Count() == 1) + { + // Current node is the target node, , return the value + return child; + } + else if (child is TwinCollection) + { + // Current node is container, go to next level + return Get(child as TwinCollection, names.Skip(1)); + } + else if (child is JContainer) + { + // Current node is container, go to next level + return Get(child as JContainer, names.Skip(1)); + } + else + { + // Currently, the container could only be TwinCollection or JContainer +#if DEBUG + throw new ApplicationException($"Unexpected TwinCollection item type: {child.GetType().FullName} @ ...{name}"); +#else + return null; +#endif + } + } + + static private dynamic Get(this JContainer container, IEnumerable names) + { + var name = names.First(); + + // Pick node on current level + var child = container[name]; + if (child == null) + { + // No desired node found. Return null as error + return null; + } + + if (names.Count() == 1) + { + // Current node is the target node, return the value + return child; + } + else if (child is JContainer) + { + // Current node is container, go to next level + return Get(child as JContainer, names.Skip(1)); + } + else + { + // The next level of JContainer must be JContainer +#if DEBUG + throw new ApplicationException($"Unexpected TwinCollection item JTokenType: {child.Type} @ {child.Path}"); +#else + return null; +#endif + } + } + + /// + /// Add/Set value of given tag or property + /// Reminder: it is not allow to set value on the node has children + /// + /// Twin.Tag, Twin.Properties.Desired or Twin.Properties.Reported + /// The flat name with prefix such as 'tags.' and so on + /// The value to be set + static public void Set(this TwinCollection collection, string flatName, dynamic value) + { + if (collection == null || string.IsNullOrWhiteSpace(flatName)) + { + throw new ArgumentNullException(); + } + + Set(collection, flatName.Split('.'), value); + } + + static private void Set(this TwinCollection collection, IEnumerable names, dynamic value) + { + var name = names.First(); + + if (names.Count() == 1) + { + // Current node is the target node, set the value + collection[name] = value; + } + else if (!collection.Contains(name)) + { + // Current node is container, go to next level + // The target collection is not exist, create and add it + // Reminder: the 'add' operation perform 'copy' rather than 'link' + var newCollection = new TwinCollection(); + Set(newCollection, names.Skip(1), value); + collection[name] = newCollection; + } + else + { + var child = collection[name]; + if (child is TwinCollection) + { + // The target collection is there. Go to next level + Set(child as TwinCollection, names.Skip(1), value); + } + else if (child is JContainer) + { + // The target collection is there. Go to next level + Set(child as JContainer, names.Skip(1), value); + } + else + { + // Currently, the container could only be TwinCollection or JContainer +#if DEBUG + throw new ApplicationException($"Unexpected TwinCollection item type: {child.GetType().FullName} @ ...{name}"); +#endif + } + } + } + + static private void Set(this JContainer container, IEnumerable names, dynamic value) + { + var name = names.First(); + + if (names.Count() == 1) + { + // Current node is the target node, set the value + container[name] = value; + } + else if (!container.Contains(name)) + { + // The target container is not exist, create and add it + var newContainer = new JObject(); + Set(newContainer, names.Skip(1), value); + container[name] = newContainer; + return; + } + else + { + // Current node is container, go to next level + var child = container[name]; + if (child is JContainer) + { + Set(child as JContainer, names.Skip(1), value); + } + else + { + // The next level of JContainer must be JContainer +#if DEBUG + throw new ApplicationException($"Unexpected TwinCollection item JTokenType: {child.Type} @ {child.Path}"); +#endif + } } } } diff --git a/Common/Extensions/TwinExtension.cs b/Common/Extensions/TwinExtension.cs index 61415830..06760377 100644 --- a/Common/Extensions/TwinExtension.cs +++ b/Common/Extensions/TwinExtension.cs @@ -39,7 +39,7 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extension if (flatName.TryTrimPrefix(selector.Key, out name)) { var collection = selector.Value(twin); - return collection.Contains(name) ? collection[name] : null; + return TwinCollectionExtension.Get(collection, name); } } @@ -68,7 +68,7 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Extension { if (flatName.TryTrimPrefix(selector.Key, out name)) { - selector.Value(twin)[name] = value; + TwinCollectionExtension.Set(selector.Value(twin), name, value); return; } } diff --git a/UnitTests/Common/TwinCollectionExtensionTests.cs b/UnitTests/Common/TwinCollectionExtensionTests.cs index 6b3ad109..7315c33c 100644 --- a/UnitTests/Common/TwinCollectionExtensionTests.cs +++ b/UnitTests/Common/TwinCollectionExtensionTests.cs @@ -48,6 +48,49 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.UnitTests.Common Assert.Equal((string)tags[7].Value.Value.Value, "Device001"); } + [Fact] + public void GetTest() + { + var twin = BuildRetrievedTwin(); + + Assert.Equal(twin.Tags.Get("DisplayName").ToString(), "Device001"); + Assert.Equal(twin.Tags.Get("Location.City").ToString(), "Beijing"); + Assert.Equal((double)twin.Tags.Get("LastTelemetry.Telemetry.Temperature"), 30.5); + + Assert.Null(twin.Tags.Get("x")); + Assert.Throws(() => twin.Tags.Get(null)); + } + + [Fact] + public void SetTest() + { + var twin = BuildRetrievedTwin(); + + // Replace leaf + twin.Tags.Set("Location.City", "Shanghai"); + Assert.Equal(twin.Tags.Get("Location.City").ToString(), "Shanghai"); + + // Add leaf (same level) + twin.Tags.Set("Location.Dummy", "n/a"); + Assert.Equal(twin.Tags.Get("Location.Dummy").ToString(), "n/a"); + + // Add left (new level) + twin.Tags.Set("Location.City.Short", "SH"); + Assert.Equal(twin.Tags.Get("Location.City.Short").ToString(), "SH"); + + // Replace intermedia node + twin.Tags.Set("LastTelemetry.Telemetry", 3); + Assert.Equal((int)twin.Tags.Get("LastTelemetry.Telemetry"), 3); + + // Replace leaf + twin.Properties.Desired.Set("FirmwareVersion.Minor", 1); + Assert.Equal((int)twin.Properties.Desired.Get("FirmwareVersion.Minor"), 1); + + // Replace intermedia node + twin.Properties.Desired.Set("FirmwareVersion.Build", 1002); + Assert.Equal((int)twin.Properties.Desired.Get("FirmwareVersion.Build"), 1002); + } + private Twin BuildRetrievedTwin() { var twin = new Twin(); @@ -71,6 +114,16 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.UnitTests.Common twin.Tags["DisplayName"] = "Device001"; + var build = new TwinCollection(); + build["Year"] = 2016; + build["Month"] = 11; + + var version = new TwinCollection(); + version["Major"] = 3; + version["Minor"] = 0; + version["Build"] = build; + twin.Properties.Desired["FirmwareVersion"] = version; + return JsonConvert.DeserializeObject(twin.ToJson()); } }