diff --git a/.gitignore b/.gitignore
index 983b1f9..28690ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@
nuget.config
build
+coverage.json
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
diff --git a/AMWD.Common.Tests/AMWD.Common.Tests.csproj b/AMWD.Common.Tests/AMWD.Common.Tests.csproj
index 63f846d..5e7b74c 100644
--- a/AMWD.Common.Tests/AMWD.Common.Tests.csproj
+++ b/AMWD.Common.Tests/AMWD.Common.Tests.csproj
@@ -3,12 +3,18 @@
net6.0
false
+ true
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs b/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs
new file mode 100644
index 0000000..c366996
--- /dev/null
+++ b/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs
@@ -0,0 +1,75 @@
+using System;
+using AMWD.Common.Tests.Utils;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Security.Cryptography;
+
+namespace AMWD.Common.Tests.Extensions
+{
+ [TestClass]
+ public class CryptographyHelperExtensionsTests
+ {
+ [TestMethod]
+ public void ShouldReturnMd5Hash()
+ {
+ // arrange
+ string str = "Hello World!";
+ byte[] bytes = new byte[] { 0xaf, 0xfe };
+
+ // act
+ string strHash = str.Md5();
+ string byteHash = bytes.Md5();
+
+ // assert
+ Assert.AreEqual("ed076287532e86365e841e92bfc50d8c", strHash);
+ Assert.AreEqual("63c983de427ce9e2430ba8554d2a822f", byteHash);
+ }
+
+ [TestMethod]
+ public void ShouldReturnSha1Hash()
+ {
+ // arrange
+ string str = "Hello World!";
+ byte[] bytes = new byte[] { 0xaf, 0xfe };
+
+ // act
+ string strHash = str.Sha1();
+ string byteHash = bytes.Sha1();
+
+ // assert
+ Assert.AreEqual("2ef7bde608ce5404e97d5f042f95f89f1c232871", strHash);
+ Assert.AreEqual("ec2c39d500316044fa49f6c8f471ddec8b4fb9d1", byteHash);
+ }
+
+ [TestMethod]
+ public void ShouldReturnSha256Hash()
+ {
+ // arrange
+ string str = "Hello World!";
+ byte[] bytes = new byte[] { 0xaf, 0xfe };
+
+ // act
+ string strHash = str.Sha256();
+ string byteHash = bytes.Sha256();
+
+ // assert
+ Assert.AreEqual("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", strHash);
+ Assert.AreEqual("4e0da689dc7a51957be426d6cfb1bd860169cb25dd1ac946a2f141228217804a", byteHash);
+ }
+
+ [TestMethod]
+ public void ShouldReturnSha512Hash()
+ {
+ // arrange
+ string str = "Hello World!";
+ byte[] bytes = new byte[] { 0xaf, 0xfe };
+
+ // act
+ string strHash = str.Sha512();
+ string byteHash = bytes.Sha512();
+
+ // assert
+ Assert.AreEqual("861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8", strHash);
+ Assert.AreEqual("591098c5d470a09f0ff48a4fdb7769ab89f803eae9e23b6f9f69dd228cca46c074bbc11a5fceaa8a5f48d14d2bf19a83a629266c2c5b7d9ef34623b64cb2f8e7", byteHash);
+ }
+ }
+}
diff --git a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
index c32b41a..ad9878a 100644
--- a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
+++ b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
@@ -1,4 +1,5 @@
using System;
+using AMWD.Common.Tests.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AMWD.Common.Tests.Extensions
@@ -10,6 +11,9 @@ namespace AMWD.Common.Tests.Extensions
public void ShouldReturnUtc()
{
// arrange
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
+ using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo);
+
var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc);
var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local);
var unspecified = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Unspecified);
@@ -33,6 +37,9 @@ namespace AMWD.Common.Tests.Extensions
public void ShouldReturnLocal()
{
// arrange
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
+ using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo);
+
var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc);
var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local);
var unspecified = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Unspecified);
@@ -122,6 +129,9 @@ namespace AMWD.Common.Tests.Extensions
public void ShouldReturnCorrectLocalAlignmentDaylightSavingEnd()
{
// arrange
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
+ using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo);
+
var utcNow = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Local);
var intervalThreeSeconds = TimeSpan.FromSeconds(3);
@@ -155,6 +165,9 @@ namespace AMWD.Common.Tests.Extensions
public void ShouldReturnCorrectLocalAlignmentDaylightSavingStart()
{
// arrange
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
+ using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo);
+
var utcNow = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Local);
var intervalThreeSeconds = TimeSpan.FromSeconds(3);
diff --git a/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs b/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs
new file mode 100644
index 0000000..ca36604
--- /dev/null
+++ b/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs
@@ -0,0 +1,307 @@
+using System;
+using AMWD.Common.Tests.Utils;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace AMWD.Common.Tests.Extensions
+{
+ [TestClass]
+ public class JsonExtensionsTests
+ {
+ [TestMethod]
+ public void ShouldReturnJson()
+ {
+ // arrange
+ var testObject = new JsonTestClass();
+ string expected = @"{""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""integerValue"":42,""stringValue"":""Foo-Bar""}}";
+
+ // act
+ string json = testObject.SerializeJson();
+
+ // assert
+ Assert.IsFalse(string.IsNullOrWhiteSpace(json));
+ Assert.AreEqual(expected, json);
+ }
+
+ [TestMethod]
+ public void ShouldReturnJsonIndented()
+ {
+ // arrange
+ var testObject = new JsonTestClass();
+ string expected = @"{
+ ""stringValue"": ""Hello World!"",
+ ""isBoolTrue"": true,
+ ""floatValue"": 12.34,
+ ""doubleValue"": 21.42,
+ ""decimalValue"": 42.21,
+ ""localTimestamp"": ""2021-11-16T20:15:34+01:00"",
+ ""utcTimestamp"": ""2021-11-16T20:15:34Z"",
+ ""object"": {
+ ""integerValue"": 42,
+ ""stringValue"": ""Foo-Bar""
+ }
+}";
+
+ // act
+ string json = testObject.SerializeJson(indented: true);
+
+ // assert
+ Assert.IsFalse(string.IsNullOrWhiteSpace(json));
+ Assert.AreEqual(expected.Replace("\r", ""), json.Replace("\r", ""));
+ }
+
+ [TestMethod]
+ public void ShouldReturnJsonWithSingleQuotes()
+ {
+ // arrange
+ var testObject = new JsonTestClass
+ {
+ StringValue = "Sam's Pub"
+ };
+ string expected = "{'stringValue':'Sam\\'s Pub','isBoolTrue':true,'floatValue':12.34,'doubleValue':21.42,'decimalValue':42.21,'localTimestamp':'2021-11-16T20:15:34+01:00','utcTimestamp':'2021-11-16T20:15:34Z','object':{'integerValue':42,'stringValue':'Foo-Bar'}}";
+
+ // act
+ string json = testObject.SerializeJson(useSingleQuotes: true);
+
+ // assert
+ Assert.IsFalse(string.IsNullOrWhiteSpace(json));
+ Assert.AreEqual(expected, json);
+ }
+
+ [TestMethod]
+ public void ShouldReturnJsonWithoutCamelCase()
+ {
+ // arrange
+ var testObject = new JsonTestClass();
+ string expected = @"{""StringValue"":""Hello World!"",""IsBoolTrue"":true,""FloatValue"":12.34,""DoubleValue"":21.42,""DecimalValue"":42.21,""LocalTimestamp"":""2021-11-16T20:15:34+01:00"",""UtcTimestamp"":""2021-11-16T20:15:34Z"",""Object"":{""IntegerValue"":42,""StringValue"":""Foo-Bar""}}";
+
+ // act
+ string json = testObject.SerializeJson(useCamelCase: false);
+
+ // assert
+ Assert.IsFalse(string.IsNullOrWhiteSpace(json));
+ Assert.AreEqual(expected, json);
+ }
+
+ [TestMethod]
+ public void ShouldReturnJsonWithType()
+ {
+ // arrange
+ var testObject = new JsonTestClass();
+ string expected = @"{""$type"":""AMWD.Common.Tests.Utils.JsonTestClass, AMWD.Common.Tests"",""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""$type"":""AMWD.Common.Tests.Utils.JsonTestSubClass, AMWD.Common.Tests"",""integerValue"":42,""stringValue"":""Foo-Bar""}}";
+
+ // act
+ string json = testObject.SerializeJson(includeType: true);
+
+ // assert
+ Assert.IsFalse(string.IsNullOrWhiteSpace(json));
+ Assert.AreEqual(expected, json);
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWithoutFallback()
+ {
+ // arrange
+ string workingJson = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}";
+ string brokenJson = @"{""strValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21.12,""stringValue"":""FooBar""}}";
+ string emptyString = "";
+
+ // act
+ var workingObj = workingJson.DeserializeJson();
+ var emptyObj = emptyString.DeserializeJson();
+
+ try
+ {
+ var brokenObj = brokenJson.DeserializeJson();
+ Assert.Fail();
+ }
+ catch (JsonReaderException)
+ { }
+
+ // assert
+ Assert.IsNotNull(workingObj);
+ Assert.IsNull(emptyObj);
+
+ Assert.AreEqual("Some fancy string", workingObj.StringValue);
+ Assert.IsFalse(workingObj.IsBoolTrue);
+ Assert.AreEqual(123.45m, workingObj.DecimalValue);
+ Assert.AreEqual(DateTimeKind.Local, workingObj.LocalTimestamp.Kind);
+ Assert.AreEqual("15.11.2021 20:15:34", workingObj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+ Assert.AreEqual(DateTimeKind.Utc, workingObj.UtcTimestamp.Kind);
+ Assert.AreEqual("16.10.2021 20:15:34", workingObj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeWithFallback()
+ {
+ // arrange
+ string workingJson = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}";
+ string brokenJson = @"{""strValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21.12,""stringValue"":""FooBar""}}";
+ string emptyString = "";
+ var fallback = new JsonTestClass
+ {
+ DoubleValue = 0.815
+ };
+
+ // act
+ var workingObj = workingJson.DeserializeJson(fallback);
+ var brokenObj = brokenJson.DeserializeJson(fallback);
+ var emptyObj = emptyString.DeserializeJson(fallback);
+
+ // assert
+ Assert.IsNotNull(workingObj);
+ Assert.IsNotNull(brokenObj);
+ Assert.IsNull(emptyObj);
+
+ Assert.AreEqual("Some fancy string", workingObj.StringValue);
+ Assert.IsFalse(workingObj.IsBoolTrue);
+ Assert.AreEqual(123.45m, workingObj.DecimalValue);
+ Assert.AreEqual(21.42, workingObj.DoubleValue);
+ Assert.AreEqual(DateTimeKind.Local, workingObj.LocalTimestamp.Kind);
+ Assert.AreEqual("15.11.2021 20:15:34", workingObj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+ Assert.AreEqual(DateTimeKind.Utc, workingObj.UtcTimestamp.Kind);
+ Assert.AreEqual("16.10.2021 20:15:34", workingObj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+
+ Assert.AreEqual(fallback.StringValue, brokenObj.StringValue);
+ Assert.AreEqual(fallback.IsBoolTrue, brokenObj.IsBoolTrue);
+ Assert.AreEqual(fallback.DecimalValue, brokenObj.DecimalValue);
+ Assert.AreEqual(fallback.DoubleValue, brokenObj.DoubleValue);
+ Assert.AreEqual(fallback.LocalTimestamp.Kind, brokenObj.LocalTimestamp.Kind);
+ Assert.AreEqual(fallback.LocalTimestamp, brokenObj.LocalTimestamp);
+ Assert.AreEqual(fallback.UtcTimestamp.Kind, brokenObj.UtcTimestamp.Kind);
+ Assert.AreEqual(fallback.UtcTimestamp, brokenObj.UtcTimestamp);
+ }
+
+ [TestMethod]
+ public void ShouldDeserializeUsingPopulate()
+ {
+ // arrange
+ string json = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}";
+ var obj = new JsonTestClass();
+
+ // act
+ obj.DeserializeJson(json);
+
+ // assert
+ Assert.AreEqual("Some fancy string", obj.StringValue);
+ Assert.IsFalse(obj.IsBoolTrue);
+ Assert.AreEqual(123.45m, obj.DecimalValue);
+ Assert.AreEqual(21.42, obj.DoubleValue);
+ Assert.AreEqual(DateTimeKind.Local, obj.LocalTimestamp.Kind);
+ Assert.AreEqual("15.11.2021 20:15:34", obj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+ Assert.AreEqual(DateTimeKind.Utc, obj.UtcTimestamp.Kind);
+ Assert.AreEqual("16.10.2021 20:15:34", obj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss"));
+ }
+
+ [TestMethod]
+ public void ShouldConvertToJObject()
+ {
+ // arrange
+ var obj = new JsonTestClass
+ {
+ StringValue = "Hello JSON",
+ DecimalValue = 0.815m
+ };
+
+ // act
+ var jObj = obj.ConvertToJObject();
+
+ // assert
+ Assert.IsNotNull(jObj);
+ Assert.AreEqual(typeof(JObject), jObj.GetType());
+ Assert.AreEqual(obj.StringValue, jObj.Value("stringValue"));
+ Assert.AreEqual(obj.DecimalValue, jObj.Value("decimalValue"));
+ Assert.AreEqual(obj.LocalTimestamp.Kind, jObj.Value("localTimestamp").Kind);
+ Assert.AreEqual(obj.LocalTimestamp, jObj.Value("localTimestamp"));
+ Assert.AreEqual(obj.UtcTimestamp.Kind, jObj.Value("utcTimestamp").Kind);
+ Assert.AreEqual(obj.UtcTimestamp, jObj.Value("utcTimestamp"));
+ }
+
+ [TestMethod]
+ public void ShouldConvertToJArray()
+ {
+ // arrange
+ string[] stringArray = new[] { "one", "two", "three" };
+ var objectArray = new[]
+ {
+ new JsonTestClass { StringValue = "One" },
+ new JsonTestClass { StringValue = "Two" },
+ new JsonTestClass { StringValue = "Three" }
+ };
+
+ // act
+ var stringJArray = stringArray.ConvertToJArray();
+ var objectJArray = objectArray.ConvertToJArray();
+
+ // assert
+ Assert.IsNotNull(stringJArray);
+ Assert.AreEqual(typeof(JArray), stringJArray.GetType());
+ Assert.AreEqual(stringArray[0], stringJArray[0]);
+ Assert.AreEqual(stringArray[1], stringJArray[1]);
+ Assert.AreEqual(stringArray[2], stringJArray[2]);
+
+ Assert.IsNotNull(objectJArray);
+ Assert.AreEqual(typeof(JArray), objectJArray.GetType());
+ Assert.AreEqual(objectArray[0].StringValue, objectJArray[0].Value("stringValue"));
+ Assert.AreEqual(objectArray[1].StringValue, objectJArray[1].Value("stringValue"));
+ Assert.AreEqual(objectArray[2].StringValue, objectJArray[2].Value("stringValue"));
+ }
+
+ [TestMethod]
+ public void ShouldRunJsonTreeWithoutDefault()
+ {
+ // arrange
+ var obj = new JsonTestClass { StringValue = "Running Json", Object = new JsonTestSubClass { IntegerValue = 4711 } };
+ var jObj = obj.ConvertToJObject();
+
+ // act
+ string topLevelString = jObj.GetValue("stringValue");
+ decimal topLevelDecimal = jObj.GetValue("decimalValue");
+ int subLevelInteger = jObj.GetValue("object:integerValue");
+ string subLevelString = jObj.GetValue("object:stringValue");
+
+ string notExistingOnTopLevel = jObj.GetValue("fancyValue");
+ string notExistingOnSubLevel = jObj.GetValue("object:fancyValue");
+ int? notExistingLevel = jObj.GetValue("fancy:int");
+
+ // assert
+ Assert.AreEqual(obj.StringValue, topLevelString);
+ Assert.AreEqual(obj.DecimalValue, topLevelDecimal);
+ Assert.AreEqual(obj.Object.IntegerValue, subLevelInteger);
+ Assert.AreEqual(obj.Object.StringValue, subLevelString);
+
+ Assert.IsNull(notExistingOnTopLevel);
+ Assert.IsNull(notExistingOnSubLevel);
+ Assert.IsNull(notExistingLevel);
+ }
+
+ [TestMethod]
+ public void ShouldRunJsonTreeWithDefault()
+ {
+ // arrange
+ var obj = new JsonTestClass { StringValue = "Running Json", Object = new JsonTestSubClass { IntegerValue = 4711 } };
+ var jObj = obj.ConvertToJObject();
+
+ // act
+ string topLevelString = jObj.GetValue("stringValue", "Test String");
+ decimal topLevelDecimal = jObj.GetValue("decimalValue", 13.24m);
+ int subLevelInteger = jObj.GetValue("object:integerValue", 55);
+ string subLevelString = jObj.GetValue("object:stringValue", "Yeah!");
+
+ string notExistingOnTopLevel = jObj.GetValue("fancyValue", "Party!");
+ string notExistingOnSubLevel = jObj.GetValue("object:fancyValue", "Well Done");
+ int? notExistingLevel = jObj.GetValue("fancy:int", 13);
+
+ // assert
+ Assert.AreEqual(obj.StringValue, topLevelString);
+ Assert.AreEqual(obj.DecimalValue, topLevelDecimal);
+ Assert.AreEqual(obj.Object.IntegerValue, subLevelInteger);
+ Assert.AreEqual(obj.Object.StringValue, subLevelString);
+
+ Assert.AreEqual("Party!", notExistingOnTopLevel);
+ Assert.AreEqual("Well Done", notExistingOnSubLevel);
+ Assert.AreEqual(13, notExistingLevel);
+ }
+ }
+}
diff --git a/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs b/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs
new file mode 100644
index 0000000..ca839ce
--- /dev/null
+++ b/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs
@@ -0,0 +1,98 @@
+using System.Threading;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace AMWD.Common.Tests.Extensions
+{
+ [TestClass]
+ public class ReaderWriterLockSlimExtensionsTests
+ {
+ [TestMethod]
+ public void ShouldEnterReadLock()
+ {
+ // arrange
+ var rwLock = new ReaderWriterLockSlim();
+
+ // act
+ using var disposable = rwLock.GetReadLock();
+
+ // assert
+ Assert.IsNotNull(disposable);
+ Assert.IsTrue(rwLock.IsReadLockHeld);
+ Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld);
+ Assert.IsFalse(rwLock.IsWriteLockHeld);
+ }
+
+ [TestMethod]
+ public void ShouldEnterUpgradeableReadLock()
+ {
+ // arrange
+ var rwLock = new ReaderWriterLockSlim();
+
+ // act
+ using var disposable = rwLock.GetUpgradeableReadLock();
+
+ // assert
+ Assert.IsNotNull(disposable);
+ Assert.IsFalse(rwLock.IsReadLockHeld);
+ Assert.IsTrue(rwLock.IsUpgradeableReadLockHeld);
+ Assert.IsFalse(rwLock.IsWriteLockHeld);
+ }
+
+ [TestMethod]
+ public void ShouldEnterWriteLock()
+ {
+ // arrange
+ var rwLock = new ReaderWriterLockSlim();
+
+ // act
+ using var disposable = rwLock.GetWriteLock();
+
+ // assert
+ Assert.IsNotNull(disposable);
+ Assert.IsFalse(rwLock.IsReadLockHeld);
+ Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld);
+ Assert.IsTrue(rwLock.IsWriteLockHeld);
+ }
+
+ [TestMethod]
+ public void ShouldNotAllowWriteLockAfterReadLock()
+ {
+ // arrange
+ var rwLock = new ReaderWriterLockSlim();
+
+ // act
+ using var disposableRead = rwLock.GetReadLock();
+ try
+ {
+ using var disposaleWrite = rwLock.GetWriteLock();
+ Assert.Fail();
+ }
+ catch (LockRecursionException)
+ { }
+
+ // assert
+ Assert.IsNotNull(disposableRead);
+ Assert.IsTrue(rwLock.IsReadLockHeld);
+ Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld);
+ Assert.IsFalse(rwLock.IsWriteLockHeld);
+ }
+
+ [TestMethod]
+ public void ShouldAllowWriteLockAfterUpgradeableReadLock()
+ {
+ // arrange
+ var rwLock = new ReaderWriterLockSlim();
+
+ // act
+ using var disposableRead = rwLock.GetUpgradeableReadLock();
+ using var disposaleWrite = rwLock.GetWriteLock();
+
+ // assert
+ Assert.IsNotNull(disposableRead);
+ Assert.IsNotNull(disposaleWrite);
+ Assert.IsFalse(rwLock.IsReadLockHeld);
+ Assert.IsTrue(rwLock.IsUpgradeableReadLockHeld);
+ Assert.IsTrue(rwLock.IsWriteLockHeld);
+ }
+ }
+}
diff --git a/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs b/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs
new file mode 100644
index 0000000..5f8f501
--- /dev/null
+++ b/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Linq;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace AMWD.Common.Tests.Extensions
+{
+ [TestClass]
+ public class StringExtensionsTests
+ {
+ [TestMethod]
+ public void ShouldReturnEmptyList()
+ {
+ // arrange
+ string hex = "";
+
+ // act
+ var bytes = hex.HexToBytes();
+
+ // assert
+ Assert.IsNotNull(bytes);
+ Assert.IsFalse(bytes.Any());
+ }
+
+ [TestMethod]
+ public void ShouldReturnEmptyListWhenInvalid()
+ {
+ // arrange
+ string hex1 = "aff";
+ string hex2 = "de:ad:be:e";
+ string hex3 = "hell";
+
+ // act
+ var bytes1 = hex1.HexToBytes();
+ var bytes2 = hex2.HexToBytes(":");
+ var bytes3 = hex3.HexToBytes();
+
+ // assert
+ Assert.IsNotNull(bytes1);
+ Assert.IsFalse(bytes1.Any());
+
+ Assert.IsNotNull(bytes2);
+ Assert.IsFalse(bytes2.Any());
+
+ Assert.IsNotNull(bytes3);
+ Assert.IsFalse(bytes3.Any());
+ }
+
+ [TestMethod]
+ public void ShouldConvertHexToBytes()
+ {
+ // arrange
+ string hex = "deadbeef";
+
+ // act
+ var bytes = hex.HexToBytes();
+
+ // assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(4, bytes.Count());
+ Assert.AreEqual(0xde, bytes.ElementAt(0));
+ Assert.AreEqual(0xad, bytes.ElementAt(1));
+ Assert.AreEqual(0xbe, bytes.ElementAt(2));
+ Assert.AreEqual(0xef, bytes.ElementAt(3));
+ }
+
+ [TestMethod]
+ public void ShouldConvertHexToBytesWithDelimiter()
+ {
+ // arrange
+ string hex = "af:fe";
+
+ // act
+ var bytes = hex.HexToBytes(":");
+
+ // assert
+ Assert.IsNotNull(bytes);
+ Assert.AreEqual(2, bytes.Count());
+ Assert.AreEqual(0xaf, bytes.ElementAt(0));
+ Assert.AreEqual(0xfe, bytes.ElementAt(1));
+ }
+
+ [TestMethod]
+ public void ShouldReturnNullWhenInvalid()
+ {
+ // arrange
+ byte[] bytes1 = null;
+ byte[] bytes2 = Array.Empty();
+
+ // act
+ string hex1 = bytes1.BytesToHex();
+ string hex2 = bytes2.BytesToHex();
+
+ // assert
+ Assert.IsNull(hex1);
+ Assert.IsNull(hex2);
+ }
+
+ [TestMethod]
+ public void ShouldReturnHexString()
+ {
+ // arrange
+ byte[] bytes = new byte[] { 0xaf, 0xfe };
+
+ // act
+ string hex = bytes.BytesToHex();
+
+ // assert
+ Assert.IsNotNull(hex);
+ Assert.AreEqual("affe", hex);
+ }
+
+ [TestMethod]
+ public void ShouldReturnHexStringWithDelimiter()
+ {
+ // arrange
+ byte[] bytes = new byte[] { 0xde, 0xad, 0xbe, 0xef };
+
+ // act
+ string hex = bytes.BytesToHex("_");
+
+ // assert
+ Assert.IsNotNull(hex);
+ Assert.AreEqual("de_ad_be_ef", hex);
+ }
+
+ [TestMethod]
+ public void ShouldEncodeStringToHex()
+ {
+ // arrange
+ string plain = "Hello";
+
+ // act
+ string hex = plain.HexEncode(Encoding.UTF8);
+
+ // assert
+ Assert.AreEqual("48656c6c6f", hex);
+ }
+
+ [TestMethod]
+ public void ShouldDecodeStringFromHex()
+ {
+ // arrange
+ string hex = "48656c6c6f";
+
+ // act
+ string plain = hex.HexDecode(Encoding.UTF8);
+
+ // assert
+ Assert.AreEqual("Hello", plain);
+ }
+
+ [TestMethod]
+ public void ShouldEncodeStringToBase64()
+ {
+ // arrange
+ string plain = "Hello";
+
+ // act
+ string base64 = plain.Base64Encode(Encoding.UTF8);
+
+ // assert
+ Assert.AreEqual("SGVsbG8=", base64);
+ }
+
+ [TestMethod]
+ public void ShouldDecodeStringFromBase64()
+ {
+ // arrange
+ string base64 = "SGVsbG8=";
+
+ // act
+ string plain = base64.Base64Decode(Encoding.UTF8);
+
+ // assert
+ Assert.AreEqual("Hello", plain);
+ }
+
+ [TestMethod]
+ public void ShouldReplaceStart()
+ {
+ // arrange
+ string str = "Hello World!";
+
+ // act
+ string test1 = str.ReplaceStart("Hello", "Bye");
+ string test2 = str.ReplaceStart("World!", "Mars?");
+
+ // assert
+ Assert.AreNotEqual(str, test1);
+ Assert.AreEqual("Bye World!", test1);
+
+ Assert.AreEqual(str, test2);
+ Assert.AreNotEqual("Hello Mars?", test2);
+ }
+
+ [TestMethod]
+ public void ShouldReplaceEnd()
+ {
+ // arrange
+ string str = "Hello World!";
+
+ // act
+ string test1 = str.ReplaceEnd("Hello", "Bye");
+ string test2 = str.ReplaceEnd("World!", "Mars?");
+
+ // assert
+ Assert.AreEqual(str, test1);
+ Assert.AreNotEqual("Bye World!", test1);
+
+ Assert.AreNotEqual(str, test2);
+ Assert.AreEqual("Hello Mars?", test2);
+ }
+
+ [TestMethod]
+ public void ShouldParseDecimal()
+ {
+ // arrange
+ decimal number = 1234.56m;
+ string stringNumberEn = "1234.56";
+ string stringNumberDe = "1234,56";
+
+ // act
+ decimal numberEn = stringNumberEn.ParseDecimal();
+ decimal numberDe = stringNumberDe.ParseDecimal();
+
+ // assert
+ Assert.AreEqual(number, numberEn);
+ Assert.AreEqual(number, numberDe);
+ }
+
+ [TestMethod]
+ public void ShouldParseDecimalWithThousandsSeparator()
+ {
+ // arrange
+ decimal number = 1234.56m;
+ string stringNumberEn = "1,234.56";
+ string stringNumberDe = "1.234,56";
+
+ // act
+ decimal numberEn = stringNumberEn.ParseDecimal();
+ decimal numberDe = stringNumberDe.ParseDecimal();
+
+ // assert
+ Assert.AreEqual(number, numberEn);
+ Assert.AreEqual(number, numberDe);
+ }
+ }
+}
diff --git a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs b/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs
index 413fae0..e59fbbb 100644
--- a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs
+++ b/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs
@@ -2,6 +2,7 @@
namespace AMWD.Common.Tests.Utils
{
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
internal class CustomMultipleAttribute : Attribute
{
diff --git a/AMWD.Common.Tests/Utils/JsonTestClass.cs b/AMWD.Common.Tests/Utils/JsonTestClass.cs
new file mode 100644
index 0000000..9d1ed3b
--- /dev/null
+++ b/AMWD.Common.Tests/Utils/JsonTestClass.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace AMWD.Common.Tests.Utils
+{
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal class JsonTestClass
+ {
+ public string StringValue { get; set; } = "Hello World!";
+
+ public bool IsBoolTrue { get; set; } = true;
+
+ public float FloatValue { get; set; } = 12.34f;
+
+ public double DoubleValue { get; set; } = 21.42;
+
+ public decimal DecimalValue { get; set; } = 42.21m;
+
+ public DateTime LocalTimestamp { get; set; } = new(2021, 11, 16, 20, 15, 34, DateTimeKind.Local);
+
+ public DateTime UtcTimestamp { get; set; } = new(2021, 11, 16, 20, 15, 34, DateTimeKind.Utc);
+
+ public JsonTestSubClass Object { get; set; } = new();
+ }
+
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal class JsonTestSubClass
+ {
+ public int IntegerValue { get; set; } = 42;
+
+ public string StringValue { get; set; } = "Foo-Bar";
+ }
+}
diff --git a/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs b/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs
new file mode 100644
index 0000000..1b6d5a9
--- /dev/null
+++ b/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs
@@ -0,0 +1,26 @@
+using System;
+using ReflectionMagic;
+
+namespace AMWD.Common.Tests.Utils
+{
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public class TimeZoneInfoLocalMock : IDisposable
+ {
+ private readonly TimeZoneInfo localTimeZoneInfo;
+
+ private TimeZoneInfoLocalMock(TimeZoneInfo timeZoneInfo)
+ {
+ localTimeZoneInfo = TimeZoneInfo.Local;
+ SetLocalTimeZone(timeZoneInfo);
+ }
+
+ public static IDisposable Create(TimeZoneInfo mockTimeZoneInfo)
+ => new TimeZoneInfoLocalMock(mockTimeZoneInfo);
+
+ public void Dispose()
+ => SetLocalTimeZone(localTimeZoneInfo);
+
+ private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo)
+ => typeof(TimeZoneInfo).AsDynamicType().s_cachedData._localTimeZone = timeZoneInfo;
+ }
+}
diff --git a/AMWD.Common/Extensions/CryptographyHelperExtensions.cs b/AMWD.Common/Extensions/CryptographyHelperExtensions.cs
index 9a8ec49..2e7e6d7 100644
--- a/AMWD.Common/Extensions/CryptographyHelperExtensions.cs
+++ b/AMWD.Common/Extensions/CryptographyHelperExtensions.cs
@@ -50,14 +50,14 @@
///
/// The string to hash, using UTF-8 encoding.
/// The SHA-256 hash value, in hexadecimal notation.
- public static string Sha256(string str) => CryptographyHelper.Sha256(str);
+ public static string Sha256(this string str) => CryptographyHelper.Sha256(str);
///
/// Computes a hash from a byte array value using the SHA-256 algorithm.
///
/// The byte array.
/// The SHA-256 hash value, in hexadecimal notation.
- public static string Sha256(byte[] bytes) => CryptographyHelper.Sha256(bytes);
+ public static string Sha256(this byte[] bytes) => CryptographyHelper.Sha256(bytes);
#endregion SHA-256
diff --git a/AMWD.Common/Extensions/ExceptionExtensions.cs b/AMWD.Common/Extensions/ExceptionExtensions.cs
index d1a0aa3..bece785 100644
--- a/AMWD.Common/Extensions/ExceptionExtensions.cs
+++ b/AMWD.Common/Extensions/ExceptionExtensions.cs
@@ -5,6 +5,7 @@ namespace System
///
/// Provides extension methods for exceptions.
///
+ [Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public static class ExceptionExtensions
{
///
diff --git a/AMWD.Common/Extensions/JsonExtensions.cs b/AMWD.Common/Extensions/JsonExtensions.cs
index 7694833..95dfaa1 100644
--- a/AMWD.Common/Extensions/JsonExtensions.cs
+++ b/AMWD.Common/Extensions/JsonExtensions.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections;
+using System.Collections;
using System.Globalization;
using System.IO;
using System.Text;
@@ -36,6 +35,34 @@ namespace Newtonsoft.Json
JsonConvert.PopulateObject(json, target, jsonSerializerSettings);
}
+ ///
+ /// Deserializes a JSON string into a new instance.
+ ///
+ /// The type of the instance to deserialize.
+ /// The JSON string to read the values from.
+ /// A new instance of with the deserialized values.
+ public static T DeserializeJson(this string json)
+ => JsonConvert.DeserializeObject(json, jsonSerializerSettings);
+
+ ///
+ /// Deserializes a JSON string into a new instance or using the fallback value.
+ ///
+ /// The type of the instance to deserialize.
+ /// The JSON string to read the values from.
+ /// A fallback value when deserialization fails.
+ /// A new instance of with the deserialized values or the fallback value.
+ public static T DeserializeJson(this string json, T fallbackValue)
+ {
+ try
+ {
+ return JsonConvert.DeserializeObject(json, jsonSerializerSettings);
+ }
+ catch
+ {
+ return fallbackValue;
+ }
+ }
+
///
/// Serializes an instance to a JSON string.
///
@@ -63,29 +90,13 @@ namespace Newtonsoft.Json
a.ErrorContext.Handled = true;
};
- if (includeType)
- serializer.TypeNameHandling = TypeNameHandling.Auto;
+ serializer.TypeNameHandling = includeType ? TypeNameHandling.All : TypeNameHandling.None;
serializer.Serialize(jw, source, typeof(T));
}
return sb.ToString().Trim();
}
- ///
- /// Deserializes a JSON string into a new instance.
- ///
- /// The type of the instance to deserialize.
- /// The JSON string to read the values from.
- /// A fallback value.
- /// A new instance of with the deserialized values.
- public static T DeserializeJson(this string json, T fallback = default)
- {
- if (!string.IsNullOrWhiteSpace(json))
- return JsonConvert.DeserializeObject(json, jsonSerializerSettings);
-
- return fallback;
- }
-
///
/// Converts an object into a JObject using the custom serializer settings.
///
@@ -141,10 +152,7 @@ namespace Newtonsoft.Json
if (lvlObj == null)
return defaultValue;
- if (typeof(T) == typeof(string))
- return (T)Convert.ChangeType(lvlObj, typeof(T));
-
- return DeepConvert.ChangeType(lvlObj);
+ return DeepConvert.ChangeType(lvlObj is JValue ? ((JValue)lvlObj).Value : lvlObj.Value