Skip to content

Commit

Permalink
Refactor - Wrapping TwinCollection to ITwinProperties interface (#1841)
Browse files Browse the repository at this point in the history
* Wrap TwinCollection into ITwinProperties interface

* Add null check on IoT Hub Device Twin

* Rollback invalid cast to string.
  • Loading branch information
kbeaugrand authored Apr 3, 2023
1 parent 5e31ff1 commit e74a06a
Show file tree
Hide file tree
Showing 21 changed files with 459 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ public async Task<IActionResult> SendCloudToDeviceMessageImplementationAsync(Dev

if (twin != null)
{
var desiredReader = new TwinCollectionReader(twin.Properties.Desired, this.log);
var reportedReader = new TwinCollectionReader(twin.Properties.Reported, this.log);
var desiredReader = new TwinPropertiesReader(twin.Properties.Desired, this.log);
var reportedReader = new TwinPropertiesReader(twin.Properties.Reported, this.log);

// the device must have a DevAddr
if (!desiredReader.TryRead(TwinPropertiesConstants.DevAddr, out DevAddr _) && !reportedReader.TryRead(TwinPropertiesConstants.DevAddr, out DevAddr _))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@

namespace LoRaTools
{
using Microsoft.Azure.Devices.Shared;

public interface IDeviceTwin
{
string DeviceId { get; }

string ETag { get; }

TwinProperties Properties { get; }
ITwinPropertiesContainer Properties { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace LoRaTools
{
using System;
using Microsoft.Azure.Devices.Shared;

public interface ITwinProperties
{
long Version { get; }

dynamic this[string propertyName] { get; set; }

bool ContainsKey(string propertyName);

DateTime GetLastUpdated();

Metadata GetMetadata();

bool TryGetValue(string propertyName, out object item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace LoRaTools
{
public interface ITwinPropertiesContainer
{
public ITwinProperties Desired { get; }

public ITwinProperties Reported { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ public class IoTHubDeviceTwin : IDeviceTwin
{
internal Twin TwinInstance { get; }

public TwinProperties Properties => this.TwinInstance.Properties;
public ITwinPropertiesContainer Properties { get; }

public IoTHubDeviceTwin(Twin twin)
{
ArgumentNullException.ThrowIfNull(twin, nameof(twin));

this.TwinInstance = twin;
this.Properties = new IoTHubTwinPropertiesContainer(twin);
}

public string ETag => this.TwinInstance.ETag;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ public IoTHubLoRaDeviceTwin(Twin twin) : base(twin)
}

public string GetGatewayID()
=> TwinInstance.Properties.Desired.TryRead<string>(TwinPropertiesConstants.GatewayID, null, out var someGatewayId)
=> this.Properties.Desired.TryRead<string>(TwinPropertiesConstants.GatewayID, null, out var someGatewayId)
? someGatewayId
: string.Empty;

public string GetNwkSKey()
{
return TwinInstance.Properties.Desired.TryRead(TwinPropertiesConstants.NwkSKey, null, out string nwkSKey)
return this.Properties.Desired.TryRead(TwinPropertiesConstants.NwkSKey, null, out string nwkSKey)
? nwkSKey
: TwinInstance.Properties.Reported.TryRead(TwinPropertiesConstants.NwkSKey, null, out nwkSKey)
: this.Properties.Reported.TryRead(TwinPropertiesConstants.NwkSKey, null, out nwkSKey)
? nwkSKey
: null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace LoRaTools.IoTHubImpl
{
using System;
using Microsoft.Azure.Devices.Shared;

public class IoTHubTwinProperties : ITwinProperties
{
private readonly TwinCollection twinCollection;

public long Version => this.twinCollection.Version;

public dynamic this[string propertyName] { get => this.twinCollection[propertyName]; set => this.twinCollection[propertyName] = value; }

public IoTHubTwinProperties(TwinCollection twinCollection)
{
this.twinCollection = twinCollection;
}

public DateTime GetLastUpdated() =>
this.twinCollection.GetLastUpdated();

public Metadata GetMetadata() => this.twinCollection.GetMetadata();

public bool ContainsKey(string propertyName)
=> this.twinCollection.Contains(propertyName);

public bool TryGetValue(string propertyName, out object item)
{
item = null;

if (!this.twinCollection.Contains(propertyName))
return false;

item = this.twinCollection[propertyName];

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace LoRaTools.IoTHubImpl
{
using Microsoft.Azure.Devices.Shared;

public class IoTHubTwinPropertiesContainer : ITwinPropertiesContainer
{
public ITwinProperties Desired { get; }

public ITwinProperties Reported { get; }

public IoTHubTwinPropertiesContainer(Twin twin)
{
this.Desired = new IoTHubTwinProperties(twin?.Properties?.Desired ?? new TwinCollection());
this.Reported = new IoTHubTwinProperties(twin?.Properties?.Reported ?? new TwinCollection());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,12 @@ namespace LoRaTools.Utils
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using LoRaWan;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Logging;

public static class TwinCollectionExtensions
{
private static readonly Type StationEuiType = typeof(StationEui);
private static readonly Type DevNonceType = typeof(DevNonce);
private static readonly Type DevAddrType = typeof(DevAddr);
private static readonly Type AppSessionKeyType = typeof(AppSessionKey);
private static readonly Type AppKeyType = typeof(AppKey);
private static readonly Type NetworkSessionKeyType = typeof(NetworkSessionKey);
private static readonly Type JoinEuiType = typeof(JoinEui);
private static readonly Type NetIdType = typeof(NetId);

public static T? SafeRead<T>(this TwinCollection twinCollection, string property, T? defaultValue = default, ILogger? logger = null)
=> twinCollection.TryRead<T>(property, logger, out var someT) ? someT : defaultValue;

Expand All @@ -44,58 +33,7 @@ public static bool TryRead<T>(this TwinCollection twinCollection, string propert
// cast to object to avoid dynamic code to be generated
var some = (object)twinCollection[property];

// quick path for values that can be directly converted
if (some is Newtonsoft.Json.Linq.JValue someJValue)
{
if (someJValue.Value is T someT)
{
value = someT;
return true;
}
}

try
{
var t = typeof(T);
var tPrime = Nullable.GetUnderlyingType(t) ?? t;

// For 100% case coverage we should handle the case where type T is nullable and the token is null.
// Since this is not possible in IoT hub, we do not handle the null cases exhaustively.

if (tPrime == StationEuiType)
value = (T)(object)StationEui.Parse(some.ToString());
else if (tPrime == DevNonceType)
value = (T)(object)new DevNonce(Convert.ToUInt16(some, CultureInfo.InvariantCulture));
else if (tPrime == DevAddrType)
value = (T)(object)DevAddr.Parse(some.ToString());
else if (tPrime == AppSessionKeyType)
value = (T)(object)AppSessionKey.Parse(some.ToString());
else if (tPrime == AppKeyType)
value = (T)(object)AppKey.Parse(some.ToString());
else if (tPrime == NetworkSessionKeyType)
value = (T)(object)NetworkSessionKey.Parse(some.ToString());
else if (tPrime == JoinEuiType)
value = (T)(object)JoinEui.Parse(some.ToString());
else if (tPrime == NetIdType)
value = (T)(object)NetId.Parse(some.ToString());
else
value = (T)Convert.ChangeType(some, t, CultureInfo.InvariantCulture);
if (t.IsEnum && !t.IsEnumDefined(value))
{
LogParsingError(logger, property, some);
return false;
}
}
catch (Exception ex) when (ex is ArgumentException
or InvalidCastException
or FormatException
or OverflowException
or Newtonsoft.Json.JsonSerializationException)
{
LogParsingError(logger, property, some, ex);
return false;
}
return true;
return TwinPropertyParser.TryParse<T>(property, some, logger, out value);
}

public static bool TryReadJsonBlock(this TwinCollection twinCollection, string property, [NotNullWhen(true)] out string? json)
Expand Down Expand Up @@ -126,8 +64,5 @@ public static bool TryParseJson<T>(this TwinCollection twinCollection, string pr
}
return value != null;
}

private static void LogParsingError(ILogger? logger, string property, object? value, Exception? ex = default)
=> logger?.LogError(ex, "Failed to parse twin '{TwinProperty}'. The value stored is '{TwinValue}'", property, value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

namespace LoRaTools.Utils
{
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;

public static class TwinPropertiesExtensions
{
public static T? SafeRead<T>(this ITwinProperties twinCollection, string property, T? defaultValue = default, ILogger? logger = null)
=> twinCollection.TryRead<T>(property, logger, out var someT) ? someT : defaultValue;

public static bool TryRead<T>(this ITwinProperties twinCollection, string property, ILogger? logger, [NotNullWhen(true)] out T? value)
{
_ = twinCollection ?? throw new ArgumentNullException(nameof(twinCollection));

value = default;

if (!twinCollection.TryGetValue(property, out var some))
return false;

return TwinPropertyParser.TryParse<T>(property, some, logger, out value);
}

public static bool TryReadJsonBlock(this ITwinProperties twinCollection, string property, [NotNullWhen(true)] out string? json)
{
_ = twinCollection ?? throw new ArgumentNullException(nameof(twinCollection));
json = null;

if (!twinCollection.TryGetValue(property, out var some))
return false;

json = some.ToString();
return json != null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

namespace LoRaTools.Utils
{
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;

public sealed class TwinPropertiesReader
{
private readonly ITwinProperties twinCollection;
private readonly ILogger logger;

public TwinPropertiesReader(ITwinProperties twinCollection, ILogger logger)
{
this.twinCollection = twinCollection;
this.logger = logger;
}

public T? SafeRead<T>(string property, T? defaultValue = default)
=> this.twinCollection.SafeRead(property, defaultValue, this.logger);

public bool TryRead<T>(string property, [NotNullWhen(true)] out T? value)
=> this.twinCollection.TryRead(property, this.logger, out value);
}
}
Loading

0 comments on commit e74a06a

Please sign in to comment.