diff --git a/src/Nancy.Tests/Nancy.Tests.csproj b/src/Nancy.Tests/Nancy.Tests.csproj index c5d8147cb9..55cf2d1d26 100644 --- a/src/Nancy.Tests/Nancy.Tests.csproj +++ b/src/Nancy.Tests/Nancy.Tests.csproj @@ -147,6 +147,7 @@ + diff --git a/src/Nancy.Tests/Unit/ModelBinding/BindingMemberInfoFixture.cs b/src/Nancy.Tests/Unit/ModelBinding/BindingMemberInfoFixture.cs new file mode 100644 index 0000000000..39a344e6ea --- /dev/null +++ b/src/Nancy.Tests/Unit/ModelBinding/BindingMemberInfoFixture.cs @@ -0,0 +1,235 @@ +namespace Nancy.Tests.Unit.ModelBinding +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Serialization; + using Nancy.ModelBinding; + using Xunit; + using Xunit.Sdk; + + public class BindingMemberInfoFixture + { + [Fact] + public void Should_return_MemberInfo_for_properties_or_fields() + { + // Given + var type = typeof(TestModel); + var underlyingFieldInfo = type.GetFields().First(); + var underlyingPropertyInfo = type.GetProperties().First(); + + // When + var fieldInfo = new BindingMemberInfo(underlyingFieldInfo); + var propertyInfo = new BindingMemberInfo(underlyingPropertyInfo); + + // Then + fieldInfo.MemberInfo.ShouldEqual(underlyingFieldInfo); + propertyInfo.MemberInfo.ShouldEqual(underlyingPropertyInfo); + } + + [Fact] + public void Should_return_Name_for_properties_or_fields() + { + // Given + var type = typeof(TestModel); + var underlyingFieldInfo = type.GetFields().First(); + var underlyingPropertyInfo = type.GetProperties().First(); + + // When + var fieldInfo = new BindingMemberInfo(underlyingFieldInfo); + var propertyInfo = new BindingMemberInfo(underlyingPropertyInfo); + + // Then + fieldInfo.Name.ShouldEqual(underlyingFieldInfo.Name); + propertyInfo.Name.ShouldEqual(underlyingPropertyInfo.Name); + } + + [Fact] + public void Should_return_PropertyType_for_properties_or_fields() + { + // Given + var properties = BindingMemberInfo.Collect(); + + // When + + // Then + properties.ShouldHaveCount(4); + + foreach (var propInfo in properties) + { + if (propInfo.Name.StartsWith("Int")) + { + propInfo.PropertyType.ShouldEqual(typeof(int)); + } + else if (propInfo.Name.StartsWith("String")) + { + propInfo.PropertyType.ShouldEqual(typeof(string)); + } + else + { + throw new AssertException("Internal error in unit test: Test model property/field name does not follow the expected convention: " + propInfo.Name); + } + } + } + + [Fact] + public void Should_get_fields() + { + // Given + var propInfo = BindingMemberInfo.Collect().Where(prop => prop.Name.EndsWith("Field")); + var model = new TestModel(); + + // When + model.IntField = 669; + model.StringField = "testing"; + + // Then + propInfo.Single(prop => prop.PropertyType == typeof(int)) + .GetValue(model) + .ShouldEqual(669); + + propInfo.Single(prop => prop.PropertyType == typeof(string)) + .GetValue(model) + .ShouldEqual("testing"); + } + + [Fact] + public void Should_set_fields() + { + // Given + var propInfo = BindingMemberInfo.Collect().Where(prop => prop.Name.EndsWith("Field")); + var model = new TestModel(); + + // When + propInfo.Single(prop => prop.PropertyType == typeof(int)) + .SetValue(model, 42); + + propInfo.Single(prop => prop.PropertyType == typeof(string)) + .SetValue(model, "nineteen"); + + // Then + model.IntField.ShouldEqual(42); + model.StringField.ShouldEqual("nineteen"); + } + + [Fact] + public void Should_get_properties() + { + // Given + var propInfo = BindingMemberInfo.Collect().Where(prop => prop.Name.EndsWith("Property")); + var model = new TestModel(); + + // When + model.IntProperty = 1701; + model.StringProperty = "NancyFX Unit Testing"; + + // Then + propInfo.Single(prop => prop.PropertyType == typeof(int)) + .GetValue(model) + .ShouldEqual(1701); + + propInfo.Single(prop => prop.PropertyType == typeof(string)) + .GetValue(model) + .ShouldEqual("NancyFX Unit Testing"); + } + + [Fact] + public void Should_set_properties() + { + // Given + var propInfo = BindingMemberInfo.Collect().Where(prop => prop.Name.EndsWith("Property")); + var model = new TestModel(); + + // When + propInfo.Single(prop => prop.PropertyType == typeof(int)) + .SetValue(model, 2600); + + propInfo.Single(prop => prop.PropertyType == typeof(string)) + .SetValue(model, "R2D2"); + + // Then + model.IntProperty.ShouldEqual(2600); + model.StringProperty.ShouldEqual("R2D2"); + } + + [Fact] + public void Should_collect_all_bindable_members_and_skip_all_others() + { + // Given + + // When + var properties = BindingMemberInfo.Collect(); + + // Then + properties.ShouldHaveCount(16); + + foreach (var property in properties) + property.Name.ShouldStartWith("Bindable"); + } + + public class TestModel + { + public int IntField; + public string StringField; + + public int IntProperty { get; set; } + public string StringProperty { get; set; } + } + + public class BiggerTestModel + { + public int BindableIntField; + public int BindableIntProperty { get; set; } + public string BindableStringField; + public string BindableStringProperty { get; set; } + public TestModel BindableTestModelField; + public TestModel BindableTestModelProperty { get; set; } + public BiggerTestModel BindableBiggerTestModelField; + public BiggerTestModel BindableBiggerTestModelProperty { get; set; } + [XmlIgnore] + public IEnumerable BindableEnumerableField; + [XmlIgnore] + public IEnumerable BindableEnumerableProperty { get; set; } + public List BindableListField; + public List BindableListProperty { get; set; } + public double[] BindableArrayField; + public double[] BindableArrayProperty { get; set; } + public TestModel[] BindableArrayOfObjectsField; + public TestModel[] BindableArrayOfObjectsProperty { get; set; } + + public int this[int index] + { + get { return 0; } + set { } + } + + public int this[string index, int index2] + { + get { return 0; } + set { } + } + + public readonly int UnbindableReadOnlyField = 74205; + + public string UnbindableReadOnlyProperty + { + get { return "hi"; } + } + + public string UnbindableWriteOnlyProperty + { + set { } + } + + private int UnbindablePrivateField; + + public static int UnbindableStaticField; + + public static int UnbindableStaticProperty + { + get { return 0; } + set { } + } + } + } +} diff --git a/src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs b/src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs index 475af207b8..f027a3cb6f 100644 --- a/src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs +++ b/src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs @@ -195,7 +195,13 @@ public void Should_use_object_from_deserializer_if_one_returned_and_overwrite_wh public void Should_use_object_from_deserializer_if_one_returned_and_not_overwrite_when_not_allowed() { // Given - var modelObject = new TestModel { StringPropertyWithDefaultValue = "Hello!" }; + var modelObject = + new TestModel() + { + StringPropertyWithDefaultValue = "Hello!", + StringFieldWithDefaultValue = "World!", + }; + var deserializer = A.Fake(); A.CallTo(() => deserializer.CanDeserialize(null, A._)).WithAnyArguments().Returns(true); A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments().Returns(modelObject); @@ -208,7 +214,8 @@ public void Should_use_object_from_deserializer_if_one_returned_and_not_overwrit // Then result.ShouldBeOfType(); - ((TestModel)result).StringPropertyWithDefaultValue.ShouldEqual("Default Value"); + ((TestModel)result).StringPropertyWithDefaultValue.ShouldEqual("Default Property Value"); + ((TestModel)result).StringFieldWithDefaultValue.ShouldEqual("Default Field Value"); } [Fact] @@ -350,7 +357,7 @@ public void Should_ignore_indexer_properties() binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); // Then - validProperties.ShouldEqual(11); + validProperties.ShouldEqual(22); } [Fact] @@ -697,10 +704,14 @@ public void Should_be_able_to_bind_more_than_once_should_ignore_non_list_propert context.Request.Form["IntProperty"] = "3"; context.Request.Form["NestedIntProperty[0]"] = "1"; + context.Request.Form["NestedIntField[0]"] = "2"; context.Request.Form["NestedStringProperty[0]"] = "one"; + context.Request.Form["NestedStringField[0]"] = "two"; - context.Request.Form["NestedIntProperty[1]"] = "2"; - context.Request.Form["NestedStringProperty[1]"] = "two"; + context.Request.Form["NestedIntProperty[1]"] = "3"; + context.Request.Form["NestedIntField[1]"] = "4"; + context.Request.Form["NestedStringProperty[1]"] = "three"; + context.Request.Form["NestedStringField[1]"] = "four"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); @@ -710,10 +721,15 @@ public void Should_be_able_to_bind_more_than_once_should_ignore_non_list_propert result.StringProperty.ShouldEqual("Test"); result.IntProperty.ShouldEqual(3); + result2.ShouldHaveCount(2); result2.First().NestedIntProperty.ShouldEqual(1); + result2.First().NestedIntField.ShouldEqual(2); result2.First().NestedStringProperty.ShouldEqual("one"); - result2.Last().NestedIntProperty.ShouldEqual(2); - result2.Last().NestedStringProperty.ShouldEqual("two"); + result2.First().NestedStringField.ShouldEqual("two"); + result2.Last().NestedIntProperty.ShouldEqual(3); + result2.Last().NestedIntField.ShouldEqual(4); + result2.Last().NestedStringProperty.ShouldEqual("three"); + result2.Last().NestedStringField.ShouldEqual("four"); } [Fact] @@ -756,13 +772,17 @@ public void Should_bind_to_IEnumerable_from_Form() var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); - context.Request.Form["IntValues"] = "1,2,3,4"; + context.Request.Form["IntValuesProperty"] = "1,2,3,4"; + context.Request.Form["IntValuesField"] = "5,6,7,8"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); // Then - result.IntValues.ShouldHaveCount(4); + result.IntValuesProperty.ShouldHaveCount(4); + result.IntValuesProperty.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); + result.IntValuesField.ShouldHaveCount(4); + result.IntValuesField.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); } [Fact] @@ -774,17 +794,24 @@ public void Should_bind_to_IEnumerable_from_Form_with_multiple_inputs() var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); - context.Request.Form["IntValues_0"] = "1,2,3,4"; - context.Request.Form["IntValues_1"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty_0"] = "1,2,3,4"; + context.Request.Form["IntValuesField_0"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty_1"] = "9,10,11,12"; + context.Request.Form["IntValuesField_1"] = "13,14,15,16"; // When var result = (List)binder.Bind(context, typeof(List), null, BindingConfig.Default); // Then - result.First().IntValues.ShouldHaveCount(4); - result.First().IntValues.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); - result.Last().IntValues.ShouldHaveCount(4); - result.Last().IntValues.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.ShouldHaveCount(2); + result.First().IntValuesProperty.ShouldHaveCount(4); + result.First().IntValuesProperty.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); + result.First().IntValuesField.ShouldHaveCount(4); + result.First().IntValuesField.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.Last().IntValuesProperty.ShouldHaveCount(4); + result.Last().IntValuesProperty.ShouldEqualSequence(new[] { 9, 10, 11, 12 }); + result.Last().IntValuesField.ShouldHaveCount(4); + result.Last().IntValuesField.ShouldEqualSequence(new[] { 13, 14, 15, 16 }); } @@ -797,18 +824,26 @@ public void Should_bind_to_IEnumerable_from_Form_with_multiple_inputs_using_brac var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); - context.Request.Form["IntValues[0]"] = "1,2,3,4"; - context.Request.Form["IntValues[1]"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty[0]"] = "1,2,3,4"; + context.Request.Form["IntValuesField[0]"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty[1]"] = "9,10,11,12"; + context.Request.Form["IntValuesField[1]"] = "13,14,15,16"; // When - var result = (List)binder.Bind(context, typeof(List), new List { new TestModel {AnotherStringProprety = "Test"} }, new BindingConfig { Overwrite = false}); + var result = (List)binder.Bind(context, typeof(List), new List { new TestModel {AnotherStringProperty = "Test"} }, new BindingConfig { Overwrite = false}); // Then - result.First().AnotherStringProprety.ShouldEqual("Test"); - result.First().IntValues.ShouldHaveCount(4); - result.First().IntValues.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); - result.Last().IntValues.ShouldHaveCount(4); - result.Last().IntValues.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.ShouldHaveCount(2); + result.First().AnotherStringProperty.ShouldEqual("Test"); + result.First().IntValuesProperty.ShouldHaveCount(4); + result.First().IntValuesProperty.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); + result.First().IntValuesField.ShouldHaveCount(4); + result.First().IntValuesField.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.Last().AnotherStringProperty.ShouldBeNull(); + result.Last().IntValuesProperty.ShouldHaveCount(4); + result.Last().IntValuesProperty.ShouldEqualSequence(new[] { 9, 10, 11, 12 }); + result.Last().IntValuesField.ShouldHaveCount(4); + result.Last().IntValuesField.ShouldEqualSequence(new[] { 13, 14, 15, 16 }); } [Fact] @@ -820,17 +855,24 @@ public void Should_bind_to_IEnumerable_from_Form_with_multiple_inputs_using_brac var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); - context.Request.Form["IntValues[0]"] = "1,2,3,4"; - context.Request.Form["IntValues[1]"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty[0]"] = "1,2,3,4"; + context.Request.Form["IntValuesField[0]"] = "5,6,7,8"; + context.Request.Form["IntValuesProperty[1]"] = "9,10,11,12"; + context.Request.Form["IntValuesField[1]"] = "13,14,15,16"; // When var result = (List)binder.Bind(context, typeof(List), null, BindingConfig.Default); // Then - result.First().IntValues.ShouldHaveCount(4); - result.First().IntValues.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); - result.Last().IntValues.ShouldHaveCount(4); - result.Last().IntValues.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.ShouldHaveCount(2); + result.First().IntValuesProperty.ShouldHaveCount(4); + result.First().IntValuesProperty.ShouldEqualSequence(new[] { 1, 2, 3, 4 }); + result.First().IntValuesField.ShouldHaveCount(4); + result.First().IntValuesField.ShouldEqualSequence(new[] { 5, 6, 7, 8 }); + result.Last().IntValuesProperty.ShouldHaveCount(4); + result.Last().IntValuesProperty.ShouldEqualSequence(new[] { 9, 10, 11, 12 }); + result.Last().IntValuesField.ShouldHaveCount(4); + result.Last().IntValuesField.ShouldEqualSequence(new[] { 13, 14, 15, 16 }); } [Fact] @@ -851,6 +893,15 @@ public void Should_bind_collections_regardless_of_case() context.Request.Form["LowercaseIntproperty[6]"] = "7"; context.Request.Form["LowercaseIntproperty[7]"] = "8"; + context.Request.Form["lowercaseintfield[0]"] = "9"; + context.Request.Form["lowercaseintfield[1]"] = "10"; + context.Request.Form["lowercaseIntfield[2]"] = "11"; + context.Request.Form["lowercaseIntfield[3]"] = "12"; + context.Request.Form["Lowercaseintfield[4]"] = "13"; + context.Request.Form["Lowercaseintfield[5]"] = "14"; + context.Request.Form["LowercaseIntfield[6]"] = "15"; + context.Request.Form["LowercaseIntfield[7]"] = "16"; + // When var result = (List)binder.Bind(context, typeof(List), null, BindingConfig.Default); @@ -858,6 +909,8 @@ public void Should_bind_collections_regardless_of_case() result.ShouldHaveCount(8); result.First().lowercaseintproperty.ShouldEqual(1); result.Last().lowercaseintproperty.ShouldEqual(8); + result.First().lowercaseintfield.ShouldEqual(9); + result.Last().lowercaseintfield.ShouldEqual(16); } [Fact] @@ -1151,7 +1204,7 @@ public void Should_bind_ienumerable_model_with_instance_from_body() var context = CreateContextWithHeaderAndBody("Content-Type", new[] { "application/json" }, body); var then = DateTime.Now; - var instance = new List { new TestModel{ DateProperty = then }, new TestModel { IntProperty = 9, AnotherStringProprety = "Bananas" } }; + var instance = new List { new TestModel{ DateProperty = then }, new TestModel { IntProperty = 9, AnotherStringProperty = "Bananas" } }; // When var result = (IEnumerable)binder.Bind(context, typeof(IEnumerable), instance, new BindingConfig{Overwrite = false}); @@ -1161,7 +1214,7 @@ public void Should_bind_ienumerable_model_with_instance_from_body() result.First().DateProperty.ShouldEqual(then); result.Last().StringProperty.ShouldEqual("AnotherTest"); result.Last().IntProperty.ShouldEqual(9); - result.Last().AnotherStringProprety.ShouldEqual("Bananas"); + result.Last().AnotherStringProperty.ShouldEqual("Bananas"); } [Fact] @@ -1174,7 +1227,7 @@ public void Should_bind_model_with_instance_from_body() var context = CreateContextWithHeaderAndBody("Content-Type", new[] { "application/xml" }, body); var then = DateTime.Now; - var instance = new TestModel { DateProperty = then, IntProperty = 6, AnotherStringProprety = "Beers" }; + var instance = new TestModel { DateProperty = then, IntProperty = 6, AnotherStringProperty = "Beers" }; // Wham var result = (TestModel)binder.Bind(context, typeof(TestModel), instance, new BindingConfig { Overwrite = false }); @@ -1183,7 +1236,7 @@ public void Should_bind_model_with_instance_from_body() result.StringProperty.ShouldEqual("Test"); result.DateProperty.ShouldEqual(then); result.IntProperty.ShouldEqual(6); - result.AnotherStringProprety.ShouldEqual("Beers"); + result.AnotherStringProperty.ShouldEqual("Beers"); } [Fact] @@ -1192,16 +1245,24 @@ public void Should_bind_model_from_body_that_contains_an_array() //Given var typeConverters = new ITypeConverter[] { new CollectionConverter(), new FallbackConverter() }; var binder = this.GetBinder(typeConverters, new List { new JsonBodyDeserializer() }); - var body = serializer.Serialize(new TestModel {StringProperty = "Test", SomeStrings = new[] {"E", "A", "D", "G", "B", "E"}}); - + var body = serializer.Serialize( + new TestModel + { + StringProperty = "Test", + SomeStringsProperty = new[] { "E", "A", "D", "G", "B", "E" }, + SomeStringsField = new[] { "G", "D", "A", "E" }, + }); + var context = CreateContextWithHeaderAndBody("Content-Type", new[] { "application/json" }, body); // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); // Then - result.SomeStrings.ShouldHaveCount(6); - result.SomeStrings.ShouldEqualSequence(new[] { "E", "A", "D", "G", "B", "E" }); + result.SomeStringsProperty.ShouldHaveCount(6); + result.SomeStringsProperty.ShouldEqualSequence(new[] { "E", "A", "D", "G", "B", "E" }); + result.SomeStringsField.ShouldHaveCount(4); + result.SomeStringsField.ShouldEqualSequence(new[] { "G", "D", "A", "E" }); } @@ -1213,18 +1274,31 @@ public void Should_bind_array_model_from_body_that_contains_an_array() var body = serializer.Serialize(new[] { - new TestModel {StringProperty = "Test", SomeStrings = new[] {"E", "A", "D", "G", "B", "E"}}, - new TestModel {StringProperty = "AnotherTest", SomeStrings = new[] {"E", "A", "D", "G", "B", "E"}} + new TestModel() + { + StringProperty = "Test", + SomeStringsProperty = new[] {"E", "A", "D", "G", "B", "E"}, + SomeStringsField = new[] { "G", "D", "A", "E" }, + }, + new TestModel() + { + StringProperty = "AnotherTest", + SomeStringsProperty = new[] {"E", "A", "D", "G", "B", "E"}, + SomeStringsField = new[] { "G", "D", "A", "E" }, + } }); var context = CreateContextWithHeaderAndBody("Content-Type", new[] { "application/json" }, body); // When - var result = (TestModel[])binder.Bind(context, typeof(TestModel[]), null, BindingConfig.Default, "SomeStrings"); + var result = (TestModel[])binder.Bind(context, typeof(TestModel[]), null, BindingConfig.Default, "SomeStringsProperty", "SomeStringsField"); // Then - result.First().SomeStrings.ShouldBeNull(); - result.Last().SomeStrings.ShouldBeNull(); + result.ShouldHaveCount(2); + result.First().SomeStringsProperty.ShouldBeNull(); + result.First().SomeStringsField.ShouldBeNull(); + result.Last().SomeStringsProperty.ShouldBeNull(); + result.Last().SomeStringsField.ShouldBeNull(); } @@ -1241,14 +1315,14 @@ public void Form_request_and_context_properties_should_take_precedence_over_body context.Request.Form["StringProperty"] = "From form"; context.Request.Query["IntProperty"] = "1"; - context.Parameters["AnotherStringProprety"] = "From context"; + context.Parameters["AnotherStringProperty"] = "From context"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); // Then result.StringProperty.ShouldEqual("From form"); - result.AnotherStringProprety.ShouldEqual("From context"); + result.AnotherStringProperty.ShouldEqual("From context"); result.IntProperty.ShouldEqual(1); } @@ -1265,14 +1339,14 @@ public void Form_request_and_context_properties_should_be_ignored_in_body_only_m context.Request.Form["StringProperty"] = "From form"; context.Request.Query["IntProperty"] = "1"; - context.Parameters["AnotherStringProprety"] = "From context"; + context.Parameters["AnotherStringProperty"] = "From context"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, new BindingConfig { BodyOnly = true }); // Then result.StringProperty.ShouldEqual("From body"); - result.AnotherStringProprety.ShouldBeNull(); // not in body, so default value + result.AnotherStringProperty.ShouldBeNull(); // not in body, so default value result.IntProperty.ShouldEqual(2); } @@ -1286,14 +1360,14 @@ public void Form_request_and_context_properties_should_NOT_be_used_in_body_only_ var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" }); context.Request.Form["StringProperty"] = "From form"; context.Request.Query["IntProperty"] = "1"; - context.Parameters["AnotherStringProprety"] = "From context"; + context.Parameters["AnotherStringProperty"] = "From context"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, new BindingConfig { BodyOnly = true }); // Then result.StringProperty.ShouldEqual(null); - result.AnotherStringProprety.ShouldEqual(null); + result.AnotherStringProperty.ShouldEqual(null); result.IntProperty.ShouldEqual(0); } @@ -1309,7 +1383,7 @@ public void Should_be_able_to_bind_body_request_form_and_context_properties() context.Request.Form["IntProperty"] = "0"; context.Request.Query["StringProperty"] = "From Query"; - context.Parameters["AnotherStringProprety"] = "From Context"; + context.Parameters["AnotherStringProperty"] = "From Context"; // When var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); @@ -1318,7 +1392,7 @@ public void Should_be_able_to_bind_body_request_form_and_context_properties() result.StringProperty.ShouldEqual("From Query"); result.IntProperty.ShouldEqual(0); result.DateProperty.ShouldEqual(new DateTime(2012, 8, 16)); - result.AnotherStringProprety.ShouldEqual("From Context"); + result.AnotherStringProperty.ShouldEqual("From Context"); } [Fact] @@ -1413,29 +1487,51 @@ public class TestModel { public TestModel() { - this.StringPropertyWithDefaultValue = "Default Value"; + this.StringPropertyWithDefaultValue = "Default Property Value"; + this.StringFieldWithDefaultValue = "Default Field Value"; } public string StringProperty { get; set; } - public string AnotherStringProprety { get; set; } + public string AnotherStringProperty { get; set; } + + public string StringField; + + public string AnotherStringField; public int IntProperty { get; set; } public int AnotherIntProperty { get; set; } + public int IntField; + + public int AnotherIntField; + public int lowercaseintproperty { get; set; } + public int lowercaseintfield; + public DateTime DateProperty { get; set; } + public DateTime DateField; + public string StringPropertyWithDefaultValue { get; set; } + public string StringFieldWithDefaultValue; + public double DoubleProperty { get; set; } + public double DoubleField; + [XmlIgnore] - public IEnumerable IntValues { get; set; } + public IEnumerable IntValuesProperty { get; set; } - public string[] SomeStrings { get; set; } + [XmlIgnore] + public IEnumerable IntValuesField; + + public string[] SomeStringsProperty { get; set; } + + public string[] SomeStringsField; public int this[int index] { @@ -1443,7 +1539,9 @@ public int this[int index] set { } } - public List Models { get; set; } + public List ModelsProperty { get; set; } + + public List ModelsField; } public class AnotherTestModel @@ -1451,6 +1549,10 @@ public class AnotherTestModel public string NestedStringProperty { get; set; } public int NestedIntProperty { get; set; } public double NestedDoubleProperty { get; set; } + + public string NestedStringField; + public int NestedIntField; + public double NestedDoubleField; } } diff --git a/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/JsonBodyDeserializerFixture.cs b/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/JsonBodyDeserializerFixture.cs index fdb9bacd9d..7538f2fe91 100644 --- a/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/JsonBodyDeserializerFixture.cs +++ b/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/JsonBodyDeserializerFixture.cs @@ -115,7 +115,7 @@ public void Should_deserialize_timespan() var context = new BindingContext() { DestinationType = typeof(TimeSpan), - ValidModelProperties = typeof(TimeSpan).GetProperties(), + ValidModelProperties = BindingMemberInfo.Collect().ToList(), }; // When @@ -135,13 +135,14 @@ public void Should_deserialize_list_of_primitives() var context = new BindingContext() { DestinationType = typeof(TestModel), - ValidModelProperties = typeof(TestModel).GetProperties(), + ValidModelProperties = BindingMemberInfo.Collect().ToList(), }; var model = new TestModel { - ListOfPrimitivesProperty = new List { 1, 3, 5 } + ListOfPrimitivesProperty = new List { 1, 3, 5 }, + ListOfPrimitivesField = new List { 2, 4, 6 }, }; var s = new JavaScriptSerializer(); @@ -159,6 +160,11 @@ public void Should_deserialize_list_of_primitives() result.ListOfPrimitivesProperty[0].ShouldEqual(1); result.ListOfPrimitivesProperty[1].ShouldEqual(3); result.ListOfPrimitivesProperty[2].ShouldEqual(5); + + result.ListOfPrimitivesField.ShouldHaveCount(3); + result.ListOfPrimitivesField[0].ShouldEqual(2); + result.ListOfPrimitivesField[1].ShouldEqual(4); + result.ListOfPrimitivesField[2].ShouldEqual(6); } [Fact] @@ -168,7 +174,7 @@ public void Should_deserialize_list_of_complex_objects() var context = new BindingContext() { DestinationType = typeof(TestModel), - ValidModelProperties = typeof(TestModel).GetProperties(), + ValidModelProperties = BindingMemberInfo.Collect().ToList(), }; var model = @@ -178,6 +184,11 @@ public void Should_deserialize_list_of_complex_objects() { new ModelWithStringValues() { Value1 = "one", Value2 = "two"}, new ModelWithStringValues() { Value1 = "three", Value2 = "four"} + }, + ListOfComplexObjectsField = new List + { + new ModelWithStringValues() { Value1 = "five", Value2 = "six"}, + new ModelWithStringValues() { Value1 = "seven", Value2 = "eight"} } }; @@ -197,21 +208,29 @@ public void Should_deserialize_list_of_complex_objects() result.ListOfComplexObjectsProperty[0].Value2.ShouldEqual("two"); result.ListOfComplexObjectsProperty[1].Value1.ShouldEqual("three"); result.ListOfComplexObjectsProperty[1].Value2.ShouldEqual("four"); + result.ListOfComplexObjectsField.ShouldHaveCount(2); + result.ListOfComplexObjectsField[0].Value1.ShouldEqual("five"); + result.ListOfComplexObjectsField[0].Value2.ShouldEqual("six"); + result.ListOfComplexObjectsField[1].Value1.ShouldEqual("seven"); + result.ListOfComplexObjectsField[1].Value2.ShouldEqual("eight"); } [Fact] public void Should_Deserialize_Signed_And_Unsigned_Nullable_Numeric_Types() { //Given - const string json = "{F1: 1, F2: 2, F3: 3}"; + const string json = "{P1: 1, P2: 2, P3: 3, F1: 4, F2: 5, F3: 6}"; //When var model = this.serializer.Deserialize (json); //Should - Assert.Equal (1, model.F1); - Assert.Equal ((uint)2, model.F2); - Assert.Equal ((uint)3, model.F3); + Assert.Equal (1, model.P1); + Assert.Equal ((uint)2, model.P2); + Assert.Equal ((uint)3, model.P3); + Assert.Equal (4, model.F1); + Assert.Equal ((uint)5, model.F2); + Assert.Equal ((uint)6, model.F3); } #if !__MonoCS__ @@ -286,8 +305,12 @@ public class TestModel : IEquatable public List ListOfPrimitivesProperty { get; set; } + public List ListOfPrimitivesField; + public List ListOfComplexObjectsProperty { get; set; } + public List ListOfComplexObjectsField { get; set; } + public bool Equals(TestModel other) { if (ReferenceEquals(null, other)) @@ -354,21 +377,25 @@ public class ModelWithStringValues { public string Value1 { get; set; } - public string Value2 { get; set; } + public string Value2; } public class ModelWithDoubleValues { public double Latitude { get; set; } - public double Longitude { get; set; } + public double Longitude; } public class ModelWithNullables { - public int? F1 { get; set; } - public uint F2 { get; set; } - public uint? F3 { get; set; } + public int? P1 { get; set; } + public uint P2 { get; set; } + public uint? P3 { get; set; } + + public int? F1; + public uint F2; + public uint? F3; } } \ No newline at end of file diff --git a/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/XmlBodyDeserializerfixture.cs b/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/XmlBodyDeserializerfixture.cs index 458198f039..9785472333 100644 --- a/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/XmlBodyDeserializerfixture.cs +++ b/src/Nancy.Tests/Unit/ModelBinding/DefaultBodyDeserializers/XmlBodyDeserializerfixture.cs @@ -1,6 +1,7 @@ namespace Nancy.Tests.Unit.ModelBinding.DefaultBodyDeserializers { using System.IO; + using System.Linq; using System.Text; using System.Xml.Serialization; using FakeItEasy; @@ -81,7 +82,7 @@ public void Should_deserialize_xml_model() var context = new BindingContext() { DestinationType = typeof(TestModel), - ValidModelProperties = typeof(TestModel).GetProperties(), + ValidModelProperties = BindingMemberInfo.Collect().ToArray(), }; // When diff --git a/src/Nancy/ModelBinding/BindingContext.cs b/src/Nancy/ModelBinding/BindingContext.cs index bc7412a812..1493061f4b 100644 --- a/src/Nancy/ModelBinding/BindingContext.cs +++ b/src/Nancy/ModelBinding/BindingContext.cs @@ -37,7 +37,7 @@ public class BindingContext /// /// DestinationType properties that are not black listed /// - public IEnumerable ValidModelProperties { get; set; } + public IEnumerable ValidModelProperties { get; set; } /// /// The incoming data fields diff --git a/src/Nancy/ModelBinding/BindingMemberInfo.cs b/src/Nancy/ModelBinding/BindingMemberInfo.cs new file mode 100644 index 0000000000..a2cb169a02 --- /dev/null +++ b/src/Nancy/ModelBinding/BindingMemberInfo.cs @@ -0,0 +1,140 @@ +namespace Nancy.ModelBinding +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// Represents a bindable member of a type, which can be a property or a field. + /// + public class BindingMemberInfo + { + PropertyInfo propertyInfo; + FieldInfo fieldInfo; + + /// + /// Gets a reference to the MemberInfo that this BindingMemberInfo represents. This can be a property or a field. + /// + public MemberInfo MemberInfo + { + get { return this.propertyInfo ?? (MemberInfo)this.fieldInfo; } + } + + /// + /// Gets the name of the property or field represented by this BindingMemberInfo. + /// + public string Name + { + get { return this.MemberInfo.Name; } + } + + /// + /// Gets the data type of the property or field represented by this BindingMemberInfo. + /// + public Type PropertyType + { + get + { + if (this.propertyInfo != null) + { + return this.propertyInfo.PropertyType; + } + else + { + return this.fieldInfo.FieldType; + } + } + } + + /// + /// Constructs a BindingMemberInfo instance for a property. + /// + /// The bindable property to represent. + public BindingMemberInfo(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException("propertyInfo"); + } + + this.propertyInfo = propertyInfo; + } + + /// + /// Constructs a BindingMemberInfo instance for a field. + /// + /// The bindable field to represent. + public BindingMemberInfo(FieldInfo fieldInfo) + { + if (fieldInfo == null) + { + throw new ArgumentNullException("fieldInfo"); + } + + this.fieldInfo = fieldInfo; + } + + /// + /// Gets the value from a specified object associated with the property or field represented by this BindingMemberInfo. + /// + /// The object whose property or field should be retrieved. + /// The value for this BindingMemberInfo's property or field in the specified object. + public object GetValue(object sourceObject) + { + if (this.propertyInfo != null) + { + return this.propertyInfo.GetValue(sourceObject, null); + } + else + { + return this.fieldInfo.GetValue(sourceObject); + } + } + + /// + /// Sets the value from a specified object associated with the property or field represented by this BindingMemberInfo. + /// + /// The object whose property or field should be assigned. + /// The value to assign in the specified object to this BindingMemberInfo's property or field. + public void SetValue(object destinationObject, object newValue) + { + if (this.propertyInfo != null) + { + this.propertyInfo.SetValue(destinationObject, newValue, null); + } + else + { + this.fieldInfo.SetValue(destinationObject, newValue); + } + } + + /// + /// Returns an enumerable sequence of bindable properties for the specified type. + /// + /// The type to enumerate. + /// Bindable properties. + public static IEnumerable Collect() + { + return Collect(typeof(T)); + } + + /// + /// Returns an enumerable sequence of bindable properties for the specified type. + /// + /// The type to enumerate. + /// Bindable properties. + public static IEnumerable Collect(Type type) + { + var fromProperties = type + .GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite) + .Where(property => !property.GetIndexParameters().Any()) + .Select(property => new BindingMemberInfo(property)); + + var fromFields = type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(f => !f.IsInitOnly) + .Select(field => new BindingMemberInfo(field)); + + return fromProperties.Concat(fromFields); + } + } +} diff --git a/src/Nancy/ModelBinding/DefaultBinder.cs b/src/Nancy/ModelBinding/DefaultBinder.cs index 3731e97b93..b45de22aa3 100644 --- a/src/Nancy/ModelBinding/DefaultBinder.cs +++ b/src/Nancy/ModelBinding/DefaultBinder.cs @@ -64,7 +64,7 @@ public DefaultBinder(IEnumerable typeConverters, IEnumerableModel type to bind to /// Optional existing instance /// The that should be applied during binding. - /// Blacklisted property names + /// Blacklisted binding property names /// Bound model public object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList) { @@ -123,7 +123,7 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin foreach (var modelProperty in bindingContext.ValidModelProperties) { - var existingCollectionValue = modelProperty.GetValue(genericinstance, null); + var existingCollectionValue = modelProperty.GetValue(genericinstance); var collectionStringValue = GetValue(modelProperty.Name, bindingContext, i); @@ -132,7 +132,7 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin { try { - BindProperty(modelProperty, collectionStringValue, bindingContext, genericinstance); + BindValue(modelProperty, collectionStringValue, bindingContext, genericinstance); } catch (PropertyBindingException ex) { @@ -146,7 +146,7 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin { foreach (var modelProperty in bindingContext.ValidModelProperties) { - var existingValue = modelProperty.GetValue(bindingContext.Model, null); + var existingValue = modelProperty.GetValue(bindingContext.Model); var stringValue = GetValue(modelProperty.Name, bindingContext); @@ -154,7 +154,7 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin { try { - BindProperty(modelProperty, stringValue, bindingContext); + BindValue(modelProperty, stringValue, bindingContext); } catch (PropertyBindingException ex) { @@ -178,7 +178,7 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin return bindingContext.Model; } - private bool BindingValueIsValid(string bindingValue, object existingValue, PropertyInfo modelProperty, BindingContext bindingContext) + private bool BindingValueIsValid(string bindingValue, object existingValue, BindingMemberInfo modelProperty, BindingContext bindingContext) { return (!String.IsNullOrEmpty(bindingValue) && (IsDefaultValue(existingValue, modelProperty.PropertyType) || @@ -264,7 +264,7 @@ private static void UpdateModelWithDeserializedModel(object bodyDeserializedMode foreach (var modelProperty in bindingContext.ValidModelProperties) { var existingValue = - modelProperty.GetValue(bindingContext.Model, null); + modelProperty.GetValue(bindingContext.Model); if (IsDefaultValue(existingValue, modelProperty.PropertyType) || bindingContext.Configuration.Overwrite) { @@ -301,7 +301,7 @@ private static void HandleReferenceTypeCollectionElement(BindingContext bindingC foreach (var modelProperty in bindingContext.ValidModelProperties) { - var existingValue = modelProperty.GetValue(genericTypeInstance, null); + var existingValue = modelProperty.GetValue(genericTypeInstance); if (IsDefaultValue(existingValue, modelProperty.PropertyType) || bindingContext.Configuration.Overwrite) { @@ -310,11 +310,11 @@ private static void HandleReferenceTypeCollectionElement(BindingContext bindingC } } - private static void CopyValue(PropertyInfo modelProperty, object source, object destination) + private static void CopyValue(BindingMemberInfo modelProperty, object source, object destination) { - var newValue = modelProperty.GetValue(source, null); + var newValue = modelProperty.GetValue(source); - modelProperty.SetValue(destination, newValue, null); + modelProperty.SetValue(destination, newValue); } private static bool IsDefaultValue(object existingValue, Type propertyType) @@ -332,7 +332,7 @@ private BindingContext CreateBindingContext(NancyContext context, Type modelType Context = context, DestinationType = modelType, Model = CreateModel(modelType, genericType, instance), - ValidModelProperties = GetProperties(modelType, genericType, blackList), + ValidModelProperties = GetBindingMembers(modelType, genericType, blackList).ToList(), RequestData = this.GetDataFields(context), GenericType = genericType, TypeConverters = this.typeConverters.Concat(this.defaults.DefaultTypeConverters), @@ -363,31 +363,12 @@ private IDictionary ConvertDynamicDictionary(DynamicDictionary d memberName => (string)dictionary[memberName]); } - private static void BindProperty(PropertyInfo modelProperty, string stringValue, BindingContext context) + private static void BindValue(BindingMemberInfo modelProperty, string stringValue, BindingContext context) { - var destinationType = modelProperty.PropertyType; - - var typeConverter = - context.TypeConverters.FirstOrDefault(c => c.CanConvertTo(destinationType, context)); - - if (typeConverter != null) - { - try - { - SetPropertyValue(modelProperty, context.Model, typeConverter.Convert(stringValue, destinationType, context)); - } - catch (Exception e) - { - throw new PropertyBindingException(modelProperty.Name, stringValue, e); - } - } - else if (destinationType == typeof(string)) - { - SetPropertyValue(modelProperty, context.Model, stringValue); - } + BindValue(modelProperty, stringValue, context, context.Model); } - private static void BindProperty(PropertyInfo modelProperty, string stringValue, BindingContext context, object genericInstance) + private static void BindValue(BindingMemberInfo modelProperty, string stringValue, BindingContext context, object targetInstance) { var destinationType = modelProperty.PropertyType; @@ -398,7 +379,7 @@ private static void BindProperty(PropertyInfo modelProperty, string stringValue, { try { - SetPropertyValue(modelProperty, genericInstance, typeConverter.Convert(stringValue, destinationType, context)); + SetBindingMemberValue(modelProperty, targetInstance, typeConverter.Convert(stringValue, destinationType, context)); } catch (Exception e) { @@ -407,30 +388,22 @@ private static void BindProperty(PropertyInfo modelProperty, string stringValue, } else if (destinationType == typeof(string)) { - SetPropertyValue(modelProperty, context.Model, stringValue); + SetBindingMemberValue(modelProperty, targetInstance, stringValue); } } - private static void SetPropertyValue(PropertyInfo modelProperty, object model, object value) + private static void SetBindingMemberValue(BindingMemberInfo modelProperty, object model, object value) { // TODO - catch reflection exceptions? - modelProperty.SetValue(model, value, null); + modelProperty.SetValue(model, value); } - private static IEnumerable GetProperties(Type modelType, Type genericType, IEnumerable blackList) + private static IEnumerable GetBindingMembers(Type modelType, Type genericType, IEnumerable blackList) { - if (genericType != null) - { - return genericType - .GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite && !blackList.Contains(p.Name, StringComparer.InvariantCulture)) - .Where(property => !property.GetIndexParameters().Any()); - } - else - { - return modelType - .GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite && !blackList.Contains(p.Name, StringComparer.InvariantCulture)) - .Where(property => !property.GetIndexParameters().Any()); - } + var blackListHash = new HashSet(blackList, StringComparer.InvariantCulture); + + return BindingMemberInfo.Collect(genericType ?? modelType) + .Where(member => !blackListHash.Contains(member.Name)); } private static object CreateModel(Type modelType, Type genericType, object instance) diff --git a/src/Nancy/Nancy.csproj b/src/Nancy/Nancy.csproj index de0871bb5f..b921815be8 100644 --- a/src/Nancy/Nancy.csproj +++ b/src/Nancy/Nancy.csproj @@ -179,6 +179,7 @@ +