From 0b4c58a3a9318679d2b1942aae0c7bf5c482f5f1 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Sun, 17 May 2015 00:41:12 -0700 Subject: [PATCH 1/3] Move ConfigurationBinder to Configuration --- .../ConfigurationBinder.cs | 255 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 64 +++++ .../Resources.resx | 12 + 3 files changed, 331 insertions(+) create mode 100644 src/Microsoft.Framework.ConfigurationModel/ConfigurationBinder.cs diff --git a/src/Microsoft.Framework.ConfigurationModel/ConfigurationBinder.cs b/src/Microsoft.Framework.ConfigurationModel/ConfigurationBinder.cs new file mode 100644 index 00000000..70952829 --- /dev/null +++ b/src/Microsoft.Framework.ConfigurationModel/ConfigurationBinder.cs @@ -0,0 +1,255 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.ConfigurationModel +{ + public static class ConfigurationBinder + { + public static TModel Bind(IConfiguration configuration) where TModel : new() + { + var model = new TModel(); + Bind(model, configuration); + return model; + } + + public static void Bind(object model, IConfiguration configuration) + { + if (model == null) + { + return; + } + + BindObjectProperties(model, configuration); + } + + private static void BindObjectProperties(object obj, IConfiguration configuration) + { + foreach (var property in GetAllProperties(obj.GetType().GetTypeInfo())) + { + BindProperty(property, obj, configuration); + } + } + + private static void BindProperty(PropertyInfo property, object propertyOwner, IConfiguration configuration) + { + configuration = configuration.GetConfigurationSection(property.Name); + + if (property.GetMethod == null || !property.GetMethod.IsPublic) + { + // We don't support set only properties + return; + } + + var propertyValue = property.GetValue(propertyOwner); + var hasPublicSetter = property.SetMethod != null && property.SetMethod.IsPublic; + + if (propertyValue == null && !hasPublicSetter) + { + // Property doesn't have a value and we cannot set it so there is no + // point in going further down the graph + return; + } + + propertyValue = BindType( + property.PropertyType, + propertyValue, + configuration); + + if (propertyValue != null && hasPublicSetter) + { + property.SetValue(propertyOwner, propertyValue); + } + } + + private static object BindType(Type type, object typeInstance, IConfiguration configuration) + { + var configValue = configuration.Get(null); + var typeInfo = type.GetTypeInfo(); + + if (configValue != null) + { + // Leaf nodes are always reinitialized + return CreateValueFromConfiguration(type, configValue, configuration); + } + else + { + var subkeys = configuration.GetConfigurationSections(); + if (subkeys.Count() != 0) + { + if (typeInstance == null) + { + if (typeInfo.IsInterface || typeInfo.IsAbstract) + { + throw new InvalidOperationException(Resources.FormatError_CannotActivateAbstractOrInterface(type)); + } + + bool hasParameterlessConstructor = typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); + if (!hasParameterlessConstructor) + { + throw new InvalidOperationException(Resources.FormatError_MissingParameterlessConstructor(type)); + } + + try + { + typeInstance = Activator.CreateInstance(type); + } + catch (Exception ex) + { + throw new InvalidOperationException(Resources.FormatError_FailedToActivate(type), ex); + } + } + + var collectionInterface = GetGenericOpenInterfaceImplementation(typeof(IDictionary<,>), type); + if (collectionInterface != null) + { + // Dictionary + BindDictionary(typeInstance, collectionInterface, configuration); + } + else + { + collectionInterface = GetGenericOpenInterfaceImplementation(typeof(ICollection<>), type); + if (collectionInterface != null) + { + // ICollection + BindCollection(typeInstance, collectionInterface, configuration); + } + else + { + // Something else + BindObjectProperties(typeInstance, configuration); + } + } + } + return typeInstance; + } + } + + private static void BindDictionary(object dictionary, Type iDictionaryType, IConfiguration configuration) + { + var iDictionaryTypeInfo = iDictionaryType.GetTypeInfo(); + + // It is guaranteed to have a two and only two parameters + // because this is an IDictionary + var keyType = iDictionaryTypeInfo.GenericTypeArguments[0]; + var valueType = iDictionaryTypeInfo.GenericTypeArguments[1]; + + if (keyType != typeof(string)) + { + // We only support string keys + return; + } + + var addMethod = iDictionaryTypeInfo.GetDeclaredMethod("Add"); + var subkeys = configuration.GetConfigurationSections().ToList(); + + foreach (var keyProperty in subkeys) + { + var keyConfiguration = keyProperty.Value; + + var item = BindType( + type: valueType, + typeInstance: null, + configuration: keyConfiguration); + if (item != null) + { + addMethod.Invoke(dictionary, new[] { keyProperty.Key, item }); + } + } + } + + private static void BindCollection(object collection, Type iCollectionType, IConfiguration configuration) + { + var iCollectionTypeInfo = iCollectionType.GetTypeInfo(); + + // It is guaranteed to have a one and only one parameter + // because this is an ICollection + var itemType = iCollectionTypeInfo.GenericTypeArguments[0]; + + var addMethod = iCollectionTypeInfo.GetDeclaredMethod("Add"); + var subkeys = configuration.GetConfigurationSections().ToList(); + + foreach (var keyProperty in subkeys) + { + var keyConfiguration = keyProperty.Value; + + try + { + var item = BindType( + type: itemType, + typeInstance: null, + configuration: keyConfiguration); + if (item != null) + { + addMethod.Invoke(collection, new[] { item }); + } + } + catch + { + } + } + } + + private static object CreateValueFromConfiguration(Type type, string value, IConfiguration configuration) + { + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return CreateValueFromConfiguration(Nullable.GetUnderlyingType(type), value, configuration); + } + + var configurationValue = configuration.Get(key: null); + + try + { + if (typeInfo.IsEnum) + { + return Enum.Parse(type, configurationValue); + } + else + { + return Convert.ChangeType(configurationValue, type); + } + } + catch (Exception ex) + { + throw new InvalidOperationException(Resources.FormatError_FailedBinding(configurationValue, type), ex); + } + } + + private static Type GetGenericOpenInterfaceImplementation(Type expectedOpenGeneric, Type actual) + { + var interfaces = actual.GetTypeInfo().ImplementedInterfaces; + foreach (var interfaceType in interfaces) + { + if (interfaceType.GetTypeInfo().IsGenericType && + interfaceType.GetGenericTypeDefinition() == expectedOpenGeneric) + { + return interfaceType; + } + } + + return null; + } + + private static IEnumerable GetAllProperties(TypeInfo type) + { + var allProperties = new List(); + + do + { + allProperties.AddRange(type.DeclaredProperties); + type = type.BaseType.GetTypeInfo(); + } + while (type != typeof(object).GetTypeInfo()); + + return allProperties; + } + } +} diff --git a/src/Microsoft.Framework.ConfigurationModel/Properties/Resources.Designer.cs b/src/Microsoft.Framework.ConfigurationModel/Properties/Resources.Designer.cs index d85cb192..ca42de77 100644 --- a/src/Microsoft.Framework.ConfigurationModel/Properties/Resources.Designer.cs +++ b/src/Microsoft.Framework.ConfigurationModel/Properties/Resources.Designer.cs @@ -58,6 +58,54 @@ internal static string FormatError_DuplicatedKeyInSwitchMappings(object p0) return string.Format(CultureInfo.CurrentCulture, GetString("Error_DuplicatedKeyInSwitchMappings"), p0); } + /// + /// Cannot create instance of type '{0}' because it is either abstract or an interface. + /// + internal static string Error_CannotActivateAbstractOrInterface + { + get { return GetString("Error_CannotActivateAbstractOrInterface"); } + } + + /// + /// Cannot create instance of type '{0}' because it is either abstract or an interface. + /// + internal static string FormatError_CannotActivateAbstractOrInterface(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_CannotActivateAbstractOrInterface"), p0); + } + + /// + /// Failed to convert '{0}' to type '{1}'. + /// + internal static string Error_FailedBinding + { + get { return GetString("Error_FailedBinding"); } + } + + /// + /// Failed to convert '{0}' to type '{1}'. + /// + internal static string FormatError_FailedBinding(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_FailedBinding"), p0, p1); + } + + /// + /// Failed to create instance of type '{0}'. + /// + internal static string Error_FailedToActivate + { + get { return GetString("Error_FailedToActivate"); } + } + + /// + /// Failed to create instance of type '{0}'. + /// + internal static string FormatError_FailedToActivate(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_FailedToActivate"), p0); + } + /// /// The configuration file '{0}' was not found and is not optional. /// @@ -138,6 +186,22 @@ internal static string FormatError_MissingBasePath(object p0, object p1, object return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingBasePath"), p0, p1, p2); } + /// + /// Cannot create instance of type '{0}' because it is missing a public parameterless constructor. + /// + internal static string Error_MissingParameterlessConstructor + { + get { return GetString("Error_MissingParameterlessConstructor"); } + } + + /// + /// Cannot create instance of type '{0}' because it is missing a public parameterless constructor. + /// + internal static string FormatError_MissingParameterlessConstructor(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingParameterlessConstructor"), p0); + } + /// /// No registered configuration source is capable of committing changes. /// diff --git a/src/Microsoft.Framework.ConfigurationModel/Resources.resx b/src/Microsoft.Framework.ConfigurationModel/Resources.resx index 4c28f6ed..877241d4 100644 --- a/src/Microsoft.Framework.ConfigurationModel/Resources.resx +++ b/src/Microsoft.Framework.ConfigurationModel/Resources.resx @@ -126,6 +126,15 @@ Keys in switch mappings are case-insensitive. A duplicated key '{0}' was found. + + Cannot create instance of type '{0}' because it is either abstract or an interface. + + + Failed to convert '{0}' to type '{1}'. + + + Failed to create instance of type '{0}'. + The configuration file '{0}' was not found and is not optional. @@ -141,6 +150,9 @@ Unable to resolve path '{0}'; construct this {1} with a non-null {2}. + + Cannot create instance of type '{0}' because it is missing a public parameterless constructor. + No registered configuration source is capable of committing changes. From b1c8a186729639ccb7e6896c4d5c0939cfadbf83 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Mon, 18 May 2015 04:00:55 -0700 Subject: [PATCH 2/3] Move tests --- .../ConfigurationBinderExceptionTests.cs | 143 +++++++ .../ConfigurationCollectionBindingTests.cs | 352 ++++++++++++++++++ 2 files changed, 495 insertions(+) create mode 100644 test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs create mode 100644 test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationCollectionBindingTests.cs diff --git a/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs new file mode 100644 index 00000000..3838cdc0 --- /dev/null +++ b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Framework.ConfigurationModel.Test +{ + public class ConfigurationBinderExceptionTests + { + [Fact] + public void ExceptionWhenTryingToBindToInterface() + { + var input = new Dictionary + { + {"ISomeInterfaceProperty:Subkey", "x"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var exception = Assert.Throws( + () => ConfigurationBinder.Bind(config)); + Assert.Equal( + Resources.FormatError_CannotActivateAbstractOrInterface(typeof(ISomeInterface)), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() + { + var input = new Dictionary + { + {"ClassWithoutPublicConstructorProperty:Subkey", "x"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var exception = Assert.Throws( + () => ConfigurationBinder.Bind(config)); + Assert.Equal( + Resources.FormatError_MissingParameterlessConstructor(typeof(ClassWithoutPublicConstructor)), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindToTypeThatCannotBeConverted() + { + const string IncorrectValue = "This is not an int"; + + var input = new Dictionary + { + {"IntProperty", IncorrectValue} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var exception = Assert.Throws( + () => ConfigurationBinder.Bind(config)); + Assert.NotNull(exception.InnerException); + Assert.Equal( + Resources.FormatError_FailedBinding(IncorrectValue, typeof(int)), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindToTypeThrowsWhenActivated() + { + var input = new Dictionary + { + {"ThrowsWhenActivatedProperty:subkey", "x"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var exception = Assert.Throws( + () => ConfigurationBinder.Bind(config)); + Assert.NotNull(exception.InnerException); + Assert.Equal( + Resources.FormatError_FailedToActivate(typeof(ThrowsWhenActivated)), + exception.Message); + } + + [Fact] + public void ExceptionIncludesKeyOfFailedBinding() + { + var input = new Dictionary + { + {"NestedOptionsProperty:NestedOptions2Property:ISomeInterfaceProperty:subkey", "x"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var exception = Assert.Throws( + () => ConfigurationBinder.Bind(config)); + Assert.Equal( + Resources.FormatError_CannotActivateAbstractOrInterface(typeof(ISomeInterface)), + exception.Message); + } + + private interface ISomeInterface + { + } + + private class ClassWithoutPublicConstructor + { + private ClassWithoutPublicConstructor() + { + } + } + + private class ThrowsWhenActivated + { + public ThrowsWhenActivated() + { + throw new Exception(); + } + } + + private class NestedOptions + { + public NestedOptions2 NestedOptions2Property { get; set; } + } + + private class NestedOptions2 + { + public ISomeInterface ISomeInterfaceProperty { get; set; } + } + + private class TestOptions + { + public ISomeInterface ISomeInterfaceProperty { get; set; } + + public ClassWithoutPublicConstructor ClassWithoutPublicConstructorProperty { get; set; } + + public int IntProperty { get; set; } + + public ThrowsWhenActivated ThrowsWhenActivatedProperty { get; set; } + + public NestedOptions NestedOptionsProperty { get; set; } + } + } +} diff --git a/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationCollectionBindingTests.cs b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationCollectionBindingTests.cs new file mode 100644 index 00000000..fb6add17 --- /dev/null +++ b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationCollectionBindingTests.cs @@ -0,0 +1,352 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Framework.ConfigurationModel.Test +{ + public class ConfigurationCollectionBinding + { + [Fact] + public void StringListBinding() + { + var input = new Dictionary + { + {"StringList:0", "val0"}, + {"StringList:1", "val1"}, + {"StringList:2", "val2"}, + {"StringList:x", "valx"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + var list = options.StringList; + + Assert.Equal(4, list.Count); + + Assert.Equal("val0", list[0]); + Assert.Equal("val1", list[1]); + Assert.Equal("val2", list[2]); + Assert.Equal("valx", list[3]); + } + + [Fact] + public void IntListBinding() + { + var input = new Dictionary + { + {"IntList:0", "42"}, + {"IntList:1", "43"}, + {"IntList:2", "44"}, + {"IntList:x", "45"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + var list = options.IntList; + + Assert.Equal(4, list.Count); + + Assert.Equal(42, list[0]); + Assert.Equal(43, list[1]); + Assert.Equal(44, list[2]); + Assert.Equal(45, list[3]); + } + + [Fact] + public void AlreadyInitializedListBinding() + { + var input = new Dictionary + { + {"AlreadyInitializedList:0", "val0"}, + {"AlreadyInitializedList:1", "val1"}, + {"AlreadyInitializedList:2", "val2"}, + {"AlreadyInitializedList:x", "valx"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + var list = options.AlreadyInitializedList; + + Assert.Equal(5, list.Count); + + Assert.Equal("This was here before", list[0]); + Assert.Equal("val0", list[1]); + Assert.Equal("val1", list[2]); + Assert.Equal("val2", list[3]); + Assert.Equal("valx", list[4]); + } + + [Fact] + public void CustomListBinding() + { + var input = new Dictionary + { + {"CustomList:0", "val0"}, + {"CustomList:1", "val1"}, + {"CustomList:2", "val2"}, + {"CustomList:x", "valx"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + var list = options.CustomList; + + Assert.Equal(4, list.Count); + + Assert.Equal("val0", list[0]); + Assert.Equal("val1", list[1]); + Assert.Equal("val2", list[2]); + Assert.Equal("valx", list[3]); + } + + [Fact] + public void ObjectListBinding() + { + var input = new Dictionary + { + {"ObjectList:0:Integer", "30"}, + {"ObjectList:1:Integer", "31"}, + {"ObjectList:2:Integer", "32"}, + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(3, options.ObjectList.Count); + + Assert.Equal(30, options.ObjectList[0].Integer); + Assert.Equal(31, options.ObjectList[1].Integer); + Assert.Equal(32, options.ObjectList[2].Integer); + } + + [Fact] + public void NestedListsBinding() + { + var input = new Dictionary + { + {"NestedLists:0:0", "val00"}, + {"NestedLists:0:1", "val01"}, + {"NestedLists:1:0", "val10"}, + {"NestedLists:1:1", "val11"}, + {"NestedLists:1:2", "val12"}, + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(2, options.NestedLists.Count); + Assert.Equal(2, options.NestedLists[0].Count); + Assert.Equal(3, options.NestedLists[1].Count); + + Assert.Equal("val00", options.NestedLists[0][0]); + Assert.Equal("val01", options.NestedLists[0][1]); + Assert.Equal("val10", options.NestedLists[1][0]); + Assert.Equal("val11", options.NestedLists[1][1]); + Assert.Equal("val12", options.NestedLists[1][2]); + } + + [Fact] + public void StringDictionaryBinding() + { + var input = new Dictionary + { + {"StringDictionary:abc", "val_1"}, + {"StringDictionary:def", "val_2"}, + {"StringDictionary:ghi", "val_3"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(3, options.StringDictionary.Count); + + Assert.Equal("val_1", options.StringDictionary["abc"]); + Assert.Equal("val_2", options.StringDictionary["def"]); + Assert.Equal("val_3", options.StringDictionary["ghi"]); + } + + [Fact] + public void IntDictionaryBinding() + { + var input = new Dictionary + { + {"IntDictionary:abc", "42"}, + {"IntDictionary:def", "43"}, + {"IntDictionary:ghi", "44"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(3, options.IntDictionary.Count); + + Assert.Equal(42, options.IntDictionary["abc"]); + Assert.Equal(43, options.IntDictionary["def"]); + Assert.Equal(44, options.IntDictionary["ghi"]); + } + + [Fact] + public void ObjectDictionary() + { + var input = new Dictionary + { + {"ObjectDictionary:abc:Integer", "1"}, + {"ObjectDictionary:def:Integer", "2"}, + {"ObjectDictionary:ghi:Integer", "3"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(3, options.ObjectDictionary.Count); + + Assert.Equal(1, options.ObjectDictionary["abc"].Integer); + Assert.Equal(2, options.ObjectDictionary["def"].Integer); + Assert.Equal(3, options.ObjectDictionary["ghi"].Integer); + } + + [Fact] + public void ListDictionary() + { + var input = new Dictionary + { + {"ListDictionary:abc:0", "abc_0"}, + {"ListDictionary:abc:1", "abc_1"}, + {"ListDictionary:def:0", "def_0"}, + {"ListDictionary:def:1", "def_1"}, + {"ListDictionary:def:2", "def_2"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(2, options.ListDictionary.Count); + Assert.Equal(2, options.ListDictionary["abc"].Count); + Assert.Equal(3, options.ListDictionary["def"].Count); + + Assert.Equal("abc_0", options.ListDictionary["abc"][0]); + Assert.Equal("abc_1", options.ListDictionary["abc"][1]); + Assert.Equal("def_0", options.ListDictionary["def"][0]); + Assert.Equal("def_1", options.ListDictionary["def"][1]); + Assert.Equal("def_2", options.ListDictionary["def"][2]); + } + + [Fact] + public void ListInNestedOptionBinding() + { + var input = new Dictionary + { + {"ObjectList:0:ListInNestedOption:0", "00"}, + {"ObjectList:0:ListInNestedOption:1", "01"}, + {"ObjectList:1:ListInNestedOption:0", "10"}, + {"ObjectList:1:ListInNestedOption:1", "11"}, + {"ObjectList:1:ListInNestedOption:2", "12"}, + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(2, options.ObjectList.Count); + Assert.Equal(2, options.ObjectList[0].ListInNestedOption.Count); + Assert.Equal(3, options.ObjectList[1].ListInNestedOption.Count); + + Assert.Equal("00", options.ObjectList[0].ListInNestedOption[0]); + Assert.Equal("01", options.ObjectList[0].ListInNestedOption[1]); + Assert.Equal("10", options.ObjectList[1].ListInNestedOption[0]); + Assert.Equal("11", options.ObjectList[1].ListInNestedOption[1]); + Assert.Equal("12", options.ObjectList[1].ListInNestedOption[2]); + } + + [Fact] + public void NonStringKeyDictionaryBinding() + { + var input = new Dictionary + { + {"NonStringKeyDictionary:abc", "val_1"}, + {"NonStringKeyDictionary:def", "val_2"}, + {"NonStringKeyDictionary:ghi", "val_3"} + }; + + var config = new Configuration(new MemoryConfigurationSource(input)); + + var options = ConfigurationBinder.Bind(config); + + Assert.Equal(0, options.NonStringKeyDictionary.Count); + } + + private class CustomList : List + { + // Add an overload, just to make sure binding picks the right Add method + public void Add(string a, string b) + { + } + } + + private class CustomDictionary : Dictionary + { + } + + private class NestedOptions + { + public int Integer { get; set; } + + public List ListInNestedOption { get; set; } + } + + private class OptionsWithLists + { + public OptionsWithLists() + { + AlreadyInitializedList = new List(); + AlreadyInitializedList.Add("This was here before"); + } + + public CustomList CustomList { get; set; } + + public List StringList { get; set; } + + public List IntList { get; set; } + + // This cannot be initialized because we cannot + // activate an interface + public IList StringListInterface { get; set; } + + public List> NestedLists { get; set; } + + public List AlreadyInitializedList { get; set; } + + public List ObjectList { get; set; } + } + + private class OptionsWithDictionary + { + public Dictionary IntDictionary { get; set; } + + public Dictionary StringDictionary { get; set; } + + public Dictionary ObjectDictionary { get; set; } + + public Dictionary> ListDictionary { get; set; } + + public Dictionary NonStringKeyDictionary { get; set; } + } + } +} From 35708d1d2d286b21848de3ce542167a21d864f37 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Mon, 4 May 2015 04:21:55 -0700 Subject: [PATCH 3/3] Move more tests over --- ...onTests.cs => ConfigurationBinderTests.cs} | 124 +++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) rename test/Microsoft.Framework.ConfigurationModel.Test/{ConfigurationBinderExceptionTests.cs => ConfigurationBinderTests.cs} (53%) diff --git a/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderTests.cs similarity index 53% rename from test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs rename to test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderTests.cs index 3838cdc0..573b95f3 100644 --- a/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderExceptionTests.cs +++ b/test/Microsoft.Framework.ConfigurationModel.Test/ConfigurationBinderTests.cs @@ -3,12 +3,130 @@ using System; using System.Collections.Generic; +using System.Reflection; using Xunit; namespace Microsoft.Framework.ConfigurationModel.Test { - public class ConfigurationBinderExceptionTests + public class ConfigurationBinderTests { + public class ComplexOptions + { + public ComplexOptions() + { + Nested = new NestedOptions(); + Virtual = "complex"; + } + public NestedOptions Nested { get; set; } + public int Integer { get; set; } + public bool Boolean { get; set; } + public virtual string Virtual { get; set; } + + public string PrivateSetter { get; private set; } + public string ProtectedSetter { get; protected set; } + public string InternalSetter { get; internal set; } + public static string StaticProperty { get; set; } + + public string ReadOnly + { + get { return null; } + } + } + + public class NestedOptions + { + public int Integer { get; set; } + } + + public class DerivedOptions : ComplexOptions + { + public override string Virtual + { + get + { + return base.Virtual; + } + set + { + base.Virtual = "Derived:" + value; + } + } + } + + public class NullableOptions + { + public bool? MyNullableBool { get; set; } + public int? MyNullableInt { get; set; } + public DateTime? MyNullableDateTime { get; set; } + } + + public class EnumOptions + { + public UriKind UriKind { get; set; } + } + + [Fact] + public void CanReadComplexProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var config = new Configuration(new MemoryConfigurationSource(dic)); + var options = ConfigurationBinder.Bind(config); + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + } + + [Fact] + public void CanReadInheritedProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"}, + {"Virtual","Sup"} + }; + var config = new Configuration(new MemoryConfigurationSource(dic)); + var options = ConfigurationBinder.Bind(config); + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + Assert.Equal("Derived:Sup", options.Virtual); + } + + [Fact] + public void CanReadStaticProperty() + { + var dic = new Dictionary + { + {"StaticProperty", "stuff"}, + }; + var config = new Configuration(new MemoryConfigurationSource(dic)); + var options = ConfigurationBinder.Bind(config); + Assert.Equal("stuff", ComplexOptions.StaticProperty); + } + + [Theory] + [InlineData("ReadOnly")] + [InlineData("PrivateSetter")] + [InlineData("ProtectedSetter")] + [InlineData("InternalSetter")] + public void ShouldBeIgnoredTests(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var config = new Configuration(new MemoryConfigurationSource(dic)); + var options = ConfigurationBinder.Bind(config); + Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + [Fact] public void ExceptionWhenTryingToBindToInterface() { @@ -117,7 +235,7 @@ public ThrowsWhenActivated() } } - private class NestedOptions + private class NestedOptions1 { public NestedOptions2 NestedOptions2Property { get; set; } } @@ -137,7 +255,7 @@ private class TestOptions public ThrowsWhenActivated ThrowsWhenActivatedProperty { get; set; } - public NestedOptions NestedOptionsProperty { get; set; } + public NestedOptions1 NestedOptionsProperty { get; set; } } } }