Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor - Wrapping TwinCollection to ITwinProperties interface #1841

Merged
merged 3 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
kbeaugrand marked this conversation as resolved.
Show resolved Hide resolved

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