diff --git a/PinkParrot.sln.DotSettings b/PinkParrot.sln.DotSettings
index 44e2de54f..6627add68 100644
--- a/PinkParrot.sln.DotSettings
+++ b/PinkParrot.sln.DotSettings
@@ -31,6 +31,8 @@
<?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
<?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile>
+ False
+ True
SingleQuoted
==========================================================================
$FILENAME$
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs
index 9c8b75362..3e437edab 100644
--- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs
@@ -21,6 +21,12 @@ namespace PinkParrot.Infrastructure
{
private readonly CultureInfo c = CultureInfo.InvariantCulture;
private readonly PropertiesBag bag = new PropertiesBag();
+ private readonly dynamic dynamicBag;
+
+ public PropertiesBagTest()
+ {
+ dynamicBag = bag;
+ }
[Fact]
public void Should_return_false_when_renaming_unknown_property()
@@ -63,8 +69,8 @@ namespace PinkParrot.Infrastructure
bag.Set("Key2", 1);
Assert.Equal(2, bag.Count);
- Assert.Equal(new[] { "Key1", "Key2" }, bag.PropertyNames.ToArray());
- Assert.Equal(new[] { "Key1", "Key2" }, bag.Properties.Keys.ToArray());
+ Assert.Equal(new[] {"Key1", "Key2"}, bag.PropertyNames.ToArray());
+ Assert.Equal(new[] {"Key1", "Key2"}, bag.Properties.Keys.ToArray());
}
[Fact]
@@ -93,10 +99,24 @@ namespace PinkParrot.Infrastructure
Assert.False(bag.Contains("KEY"));
}
+ [Fact]
+ public void Should_set_value_as_dynamic()
+ {
+ dynamicBag.Key = 456;
+
+ Assert.Equal(456, (int)dynamicBag.Key);
+ }
+
+ [Fact]
+ public void Should_throw_when_setting_value_with_invalid_type_dynamically()
+ {
+ Assert.Throws(() => dynamicBag.Key = (byte)123);
+ }
+
[Fact]
public void Should_throw_when_setting_value_with_invalid_type()
{
- Assert.Throws(() => bag.Set("Key", (byte)1));
+ Assert.Throws(() => bag.Set("Key", (byte)1));
}
[Fact]
@@ -168,7 +188,7 @@ namespace PinkParrot.Infrastructure
[Fact]
public void Should_convert_from_instant_string()
{
- var time = SystemClock.Instance.GetCurrentInstant();
+ var time = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds());
bag.Set("Key", time.ToString());
@@ -200,7 +220,7 @@ namespace PinkParrot.Infrastructure
{
bag.Set("Key", true);
- AssertBoolean();
+ AssertBoolean(true);
}
[Fact]
@@ -208,7 +228,7 @@ namespace PinkParrot.Infrastructure
{
bag.Set("Key", "true");
- AssertBoolean();
+ AssertBoolean(true);
}
[Fact]
@@ -216,66 +236,109 @@ namespace PinkParrot.Infrastructure
{
bag.Set("Key", 1);
- AssertBoolean();
+ AssertBoolean(true);
}
[Fact]
- public void Should_throw_when_converting_instant_to_number()
+ public void Should_convert_boolean_to_truthy_number_string()
{
- bag.Set("Key", SystemClock.Instance.GetCurrentInstant());
+ bag.Set("Key", "1");
- Assert.Throws(() => bag["Key"].ToGuid(CultureInfo.InvariantCulture));
+ AssertBoolean(true);
}
[Fact]
- public void Should_return_default_when_property_value_is_null()
+ public void Should_convert_boolean_to_falsy_number_string()
{
- bag.Set("Key", null);
+ bag.Set("Key", "0");
- Assert.Equal(null, bag["Key"].ToString());
+ AssertBoolean(false);
+ }
+
+ [Fact]
+ public void Should_throw_when_converting_instant_to_number()
+ {
+ bag.Set("Key", SystemClock.Instance.GetCurrentInstant());
- Assert.Equal(0f, bag["Key"].ToSingle(CultureInfo.CurrentCulture));
- Assert.Equal(0d, bag["Key"].ToDouble(CultureInfo.CurrentCulture));
- Assert.Equal(0L, bag["Key"].ToInt64(CultureInfo.CurrentCulture));
- Assert.Equal(0, bag["Key"].ToInt32(CultureInfo.CurrentCulture));
+ Assert.Throws(() => bag["Key"].ToGuid(CultureInfo.InvariantCulture));
+ }
- Assert.Equal(false, bag["Key"].ToBoolean(CultureInfo.CurrentCulture));
+ private void AssertNumber()
+ {
+ AssertInt32(123);
+ AssertInt64(123);
+ AssertSingle(123);
+ AssertDouble(123);
+ }
- Assert.Equal(new Guid(), bag["Key"].ToGuid(CultureInfo.CurrentCulture));
+ private void AssertString(string expected)
+ {
+ Assert.Equal(expected, bag["Key"].ToString());
- Assert.Equal(new Instant(), bag["Key"].ToInstant(CultureInfo.CurrentCulture));
+ Assert.Equal(expected, (string)dynamicBag.Key);
}
- private void AssertBoolean()
+ private void AssertBoolean(bool expected)
{
- Assert.True(bag["Key"].ToBoolean(c));
- Assert.True(bag["Key"].ToNullableBoolean(c));
+ Assert.Equal(expected, bag["Key"].ToBoolean(c));
+ Assert.Equal(expected, bag["Key"].ToNullableBoolean(c));
+
+ Assert.Equal(expected, (bool)dynamicBag.Key);
+ Assert.Equal(expected, (bool?)dynamicBag.Key);
}
private void AssertInstant(Instant expected)
{
- Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToInstant(c).ToUnixTimeSeconds());
- Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToNullableInstant(c).Value.ToUnixTimeSeconds());
+ Assert.Equal(expected, bag["Key"].ToInstant(c));
+ Assert.Equal(expected, bag["Key"].ToNullableInstant(c).Value);
+
+ Assert.Equal(expected, (Instant)dynamicBag.Key);
+ Assert.Equal(expected, (Instant?)dynamicBag.Key);
}
private void AssertGuid(Guid expected)
{
Assert.Equal(expected, bag["Key"].ToGuid(c));
Assert.Equal(expected, bag["Key"].ToNullableGuid(c));
+
+ Assert.Equal(expected, (Guid)dynamicBag.Key);
+ Assert.Equal(expected, (Guid?)dynamicBag.Key);
}
- private void AssertNumber()
+ private void AssertDouble(double expected)
+ {
+ Assert.Equal(expected, bag["Key"].ToDouble(c));
+ Assert.Equal(expected, bag["Key"].ToNullableDouble(c));
+
+ Assert.Equal(expected, (double)dynamicBag.Key);
+ Assert.Equal(expected, (double?)dynamicBag.Key);
+ }
+
+ private void AssertSingle(float expected)
+ {
+ Assert.Equal(expected, bag["Key"].ToSingle(c));
+ Assert.Equal(expected, bag["Key"].ToNullableSingle(c));
+
+ Assert.Equal(expected, (float)dynamicBag.Key);
+ Assert.Equal(expected, (float?)dynamicBag.Key);
+ }
+
+ private void AssertInt64(long expected)
+ {
+ Assert.Equal(expected, bag["Key"].ToInt64(c));
+ Assert.Equal(expected, bag["Key"].ToNullableInt64(c));
+
+ Assert.Equal(expected, (long)dynamicBag.Key);
+ Assert.Equal(expected, (long?)dynamicBag.Key);
+ }
+
+ private void AssertInt32(int expected)
{
- Assert.Equal(123, bag["Key"].ToInt32(c));
- Assert.Equal(123, bag["Key"].ToNullableInt32(c));
- Assert.Equal(123L, bag["Key"].ToInt64(c));
- Assert.Equal(123L, bag["Key"].ToNullableInt64(c));
- Assert.Equal(123f, bag["Key"].ToSingle(c));
- Assert.Equal(123f, bag["Key"].ToNullableSingle(c));
- Assert.Equal(123d, bag["Key"].ToDouble(c));
- Assert.Equal(123d, bag["Key"].ToNullableDouble(c));
+ Assert.Equal(expected, bag["Key"].ToInt32(c));
+ Assert.Equal(expected, bag["Key"].ToNullableInt32(c));
- Assert.True(bag["Key"].ToBoolean(c));
+ Assert.Equal(expected, (int)dynamicBag.Key);
+ Assert.Equal(expected, (int?)dynamicBag.Key);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs
index dc47c0a3d..5c4e1bac4 100644
--- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs
@@ -106,5 +106,16 @@ namespace PinkParrot.Infrastructure
return result;
}
+
+ public static bool TryGetValueAsObject(this IReadOnlyDictionary dictionary, TKey key, out object value)
+ {
+ TValue result;
+
+ var isFound = dictionary.TryGetValue(key, out result);
+
+ value = result;
+
+ return isFound;
+ }
}
}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs
index 4d773e167..612803f97 100644
--- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs
@@ -8,10 +8,11 @@
using System;
using System.Collections.Generic;
+using System.Dynamic;
namespace PinkParrot.Infrastructure
{
- public class PropertiesBag
+ public class PropertiesBag : DynamicObject
{
private readonly Dictionary internalDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -40,6 +41,23 @@ namespace PinkParrot.Infrastructure
}
}
+ public override IEnumerable GetDynamicMemberNames()
+ {
+ return internalDictionary.Keys;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ return internalDictionary.TryGetValueAsObject(binder.Name, out result);
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ internalDictionary[binder.Name] = new PropertyValue(value);
+
+ return true;
+ }
+
public bool Contains(string propertyName)
{
Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
@@ -47,20 +65,20 @@ namespace PinkParrot.Infrastructure
return internalDictionary.ContainsKey(propertyName);
}
- public PropertiesBag Set(string propertyName, object value)
+ public bool Remove(string propertyName)
{
Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
- internalDictionary[propertyName] = new PropertyValue(value);
-
- return this;
+ return internalDictionary.Remove(propertyName);
}
- public bool Remove(string propertyName)
+ public PropertiesBag Set(string propertyName, object value)
{
Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
- return internalDictionary.Remove(propertyName);
+ internalDictionary[propertyName] = new PropertyValue(value);
+
+ return this;
}
public bool Rename(string oldPropertyName, string newPropertyName)
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs
index 404ad969e..95c13f53b 100644
--- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs
@@ -8,35 +8,37 @@
using System;
using System.Collections.Generic;
+using System.Dynamic;
using System.Globalization;
using NodaTime;
using NodaTime.Text;
namespace PinkParrot.Infrastructure
{
- public sealed class PropertyValue
+ public sealed class PropertyValue : DynamicObject
{
private readonly object rawValue;
- private static readonly HashSet AllowedTypes = new HashSet
- {
- typeof(string),
- typeof(bool),
- typeof(bool?),
- typeof(float),
- typeof(float?),
- typeof(double),
- typeof(double?),
- typeof(int),
- typeof(int?),
- typeof(long),
- typeof(long?),
- typeof(Instant),
- typeof(Instant?),
- typeof(Guid),
- typeof(Guid?)
- };
-
+ private static readonly Dictionary> Parsers =
+ new Dictionary>
+ {
+ { typeof(string), (p, c) => p.ToString() },
+ { typeof(bool), (p, c) => p.ToBoolean(c) },
+ { typeof(bool?), (p, c) => p.ToNullableBoolean(c) },
+ { typeof(float), (p, c) => p.ToSingle(c) },
+ { typeof(float?), (p, c) => p.ToNullableSingle(c) },
+ { typeof(double), (p, c) => p.ToDouble(c) },
+ { typeof(double?), (p, c) => p.ToNullableDouble(c) },
+ { typeof(int), (p, c) => p.ToInt32(c) },
+ { typeof(int?), (p, c) => p.ToNullableInt32(c) },
+ { typeof(long), (p, c) => p.ToInt64(c) },
+ { typeof(long?), (p, c) => p.ToNullableInt64(c) },
+ { typeof(Instant), (p, c) => p.ToInstant(c) },
+ { typeof(Instant?), (p, c) => p.ToNullableInstant(c) },
+ { typeof(Guid), (p, c) => p.ToGuid(c) },
+ { typeof(Guid?), (p, c) => p.ToNullableGuid(c) },
+ };
+
public object RawValue
{
get { return rawValue; }
@@ -44,14 +46,35 @@ namespace PinkParrot.Infrastructure
internal PropertyValue(object rawValue)
{
- if (rawValue != null && !AllowedTypes.Contains(rawValue.GetType()))
+ if (rawValue != null && !Parsers.ContainsKey(rawValue.GetType()))
{
- throw new ArgumentException("The type is not supported.", nameof(rawValue));
+ throw new InvalidOperationException($"The type '{rawValue.GetType()}' is not supported.");
}
this.rawValue = rawValue;
}
-
+
+ public override bool TryConvert(ConvertBinder binder, out object result)
+ {
+ result = null;
+
+ if (binder.Type == typeof(object))
+ {
+ result = rawValue;
+ }
+
+ Func parser;
+
+ if (!Parsers.TryGetValue(binder.Type, out parser))
+ {
+ return false;
+ }
+
+ result = parser(this, CultureInfo.InvariantCulture);
+
+ return true;
+ }
+
public override string ToString()
{
return rawValue?.ToString();
@@ -59,12 +82,12 @@ namespace PinkParrot.Infrastructure
public bool ToBoolean(CultureInfo culture)
{
- return ToOrParseValue(culture, bool.Parse);
+ return ToOrParseValue(culture, ParseBoolean);
}
public bool? ToNullableBoolean(CultureInfo culture)
{
- return ToNullableOrParseValue(culture, bool.Parse);
+ return ToNullableOrParseValue(culture, ParseBoolean);
}
public float ToSingle(CultureInfo culture)
@@ -207,5 +230,18 @@ namespace PinkParrot.Infrastructure
throw new InvalidCastException(message, e);
}
}
+
+ private static bool ParseBoolean(string value)
+ {
+ switch (value)
+ {
+ case "1":
+ return true;
+ case "0":
+ return false;
+ default:
+ return bool.Parse(value);
+ }
+ }
}
}