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.
This commit is contained in:
Xiangzhi Sheng 2016-11-23 14:44:20 +08:00
Родитель 3d279c128c
Коммит e1d07fa1b8
3 изменённых файлов: 245 добавлений и 6 удалений

Просмотреть файл

@ -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
/// <returns>Enumerator returns the flat name and value</returns>
static public IEnumerable<KeyValuePair<string, TwinValue>> AsEnumerableFlatten(this TwinCollection collection, string prefix = "")
{
if (collection == null)
{
throw new ArgumentNullException();
}
foreach (KeyValuePair<string, object> 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
}
}
}
/// <summary>
/// Get value of given tag or property specified by the flat name
/// </summary>
/// <param name="collection">Twin.Tag, Twin.Properties.Desired or Twin.Properties.Reported</param>
/// <param name="flatName">The flat name with prefix such as 'tags.' and so on</param>
/// <returns>The value in dynamic, or null in case error</returns>
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<string> 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<string> 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
}
}
/// <summary>
/// Add/Set value of given tag or property
/// Reminder: it is not allow to set value on the node has children
/// </summary>
/// <param name="collection">Twin.Tag, Twin.Properties.Desired or Twin.Properties.Reported</param>
/// <param name="flatName">The flat name with prefix such as 'tags.' and so on</param>
/// <param name="value">The value to be set</param>
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<string> 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<string> 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
}
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<ArgumentNullException>(() => 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>(twin.ToJson());
}
}