diff --git a/src/Nancy/Extensions/CollectionExtensions.cs b/src/Nancy/Extensions/CollectionExtensions.cs index 605ffed977..14f319203c 100644 --- a/src/Nancy/Extensions/CollectionExtensions.cs +++ b/src/Nancy/Extensions/CollectionExtensions.cs @@ -84,5 +84,20 @@ public static IEnumerable DistinctBy(this IEnumerable + /// Tries to get value from dictionary + /// + /// + /// + /// The instance to get value from. + /// The key to lookup. + /// + public static TValue GetValueOrDefault(this IDictionary source, TKey key) + { + TValue result; + source.TryGetValue(key, out result); + return result; + } } } \ No newline at end of file diff --git a/src/Nancy/Extensions/TypeExtensions.cs b/src/Nancy/Extensions/TypeExtensions.cs index 42c2dba42d..62f4626a4e 100644 --- a/src/Nancy/Extensions/TypeExtensions.cs +++ b/src/Nancy/Extensions/TypeExtensions.cs @@ -45,6 +45,16 @@ public static Assembly GetAssembly(this Type source) return source.GetTypeInfo().Assembly; } + /// + /// Checks if a type is array, collection or enumerable + /// + /// The type to check. + /// + public static bool IsCollectionOrArrayOrEnumerable(this Type source) + { + return source.IsArray() || source.IsCollection() || source.IsEnumerable(); + } + /// /// Checks if a type is an array or not /// diff --git a/src/Nancy/ModelBinding/DefaultBinder.cs b/src/Nancy/ModelBinding/DefaultBinder.cs index d6958ee93f..e5d14cd14a 100644 --- a/src/Nancy/ModelBinding/DefaultBinder.cs +++ b/src/Nancy/ModelBinding/DefaultBinder.cs @@ -68,31 +68,39 @@ public DefaultBinder(IEnumerable typeConverters, IEnumerableBound model public object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList) { - Type genericType = null; - if (modelType.IsArray() || modelType.IsCollection() || modelType.IsEnumerable()) + var bindingContext = this.CreateBindingContext(context, modelType, instance, configuration, blackList); + + this.DeserializeBody(modelType, bindingContext); + + var bindingExceptions = new List(); + + if (!bindingContext.Configuration.BodyOnly) { - //make sure it has a generic type - if (modelType.GetTypeInfo().IsGenericType) + if (bindingContext.DestinationType.IsCollectionOrArrayOrEnumerable()) { - genericType = modelType.GetGenericArguments().FirstOrDefault(); + this.BindModelAsList(bindingContext, bindingExceptions); } else { - var ienumerable = - modelType.GetInterfaces().Where(i => i.GetTypeInfo().IsGenericType).FirstOrDefault( - i => i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - genericType = ienumerable == null ? null : ienumerable.GetGenericArguments().FirstOrDefault(); + this.BindProperties(blackList, bindingContext, bindingExceptions); } - if (genericType == null) + if (bindingExceptions.Any() && !bindingContext.Configuration.IgnoreErrors) { - throw new ArgumentException("When modelType is an enumerable it must specify the type.", "modelType"); + throw new ModelBindingException(modelType, bindingExceptions); } } - var bindingContext = - this.CreateBindingContext(context, modelType, instance, configuration, blackList, genericType); + if (modelType.IsArray()) + { + var generictoArrayMethod = ToArrayMethodInfo.MakeGenericMethod(new[] { bindingContext.GenericType }); + return generictoArrayMethod.Invoke(null, new[] { bindingContext.Model }); + } + return bindingContext.Model; + } + private void DeserializeBody(Type modelType, BindingContext bindingContext) + { try { var bodyDeserializedModel = this.DeserializeRequestBody(bindingContext); @@ -108,83 +116,104 @@ public object Bind(NancyContext context, Type modelType, object instance, Bindin throw new ModelBindingException(modelType, innerException: exception); } } + } - var bindingExceptions = new List(); + private void BindModelAsList(BindingContext bindingContext, List bindingExceptions) + { + var list = (IList)bindingContext.Model; + var loopCount = this.GetBindingListInstanceCount(bindingContext); + for (var i = 0; i < loopCount; i++) + { + this.AddInstance(list, i, bindingContext, bindingExceptions, bindingContext.GenericType, + bindingContext.ValidModelBindingMembers, + propName => GetValue(propName, bindingContext, i)); + } + } - if (!bindingContext.Configuration.BodyOnly) + private void BindProperties(string[] blackList, BindingContext bindingContext, List bindingExceptions) + { + foreach (var modelProperty in bindingContext.ValidModelBindingMembers) { - if (bindingContext.DestinationType.IsCollection() || bindingContext.DestinationType.IsArray() ||bindingContext.DestinationType.IsEnumerable()) + var existingValue = modelProperty.GetValue(bindingContext.Model); + + var stringValue = bindingContext.RequestData.GetValueOrDefault(modelProperty.Name) ?? string.Empty; + + if (this.BindingValueIsValid(stringValue, existingValue, modelProperty, bindingContext)) { - var loopCount = this.GetBindingListInstanceCount(context); - var model = (IList)bindingContext.Model; - for (var i = 0; i < loopCount; i++) + try { - object genericinstance; - if (model.Count > i) - { - genericinstance = model[i]; - } - else - { - genericinstance = bindingContext.GenericType.CreateInstance(); - model.Add(genericinstance); - } - - foreach (var modelProperty in bindingContext.ValidModelBindingMembers) - { - var existingCollectionValue = modelProperty.GetValue(genericinstance); - - var collectionStringValue = GetValue(modelProperty.Name, bindingContext, i); - - if (this.BindingValueIsValid(collectionStringValue, existingCollectionValue, modelProperty, - bindingContext)) - { - try - { - BindValue(modelProperty, collectionStringValue, bindingContext, genericinstance); - } - catch (PropertyBindingException ex) - { - bindingExceptions.Add(ex); - } - } - } + BindValue(modelProperty, stringValue, bindingContext); } - } - else - { - foreach (var modelProperty in bindingContext.ValidModelBindingMembers) + catch (PropertyBindingException ex) { - var existingValue = modelProperty.GetValue(bindingContext.Model); - - var stringValue = GetValue(modelProperty.Name, bindingContext); - - if (this.BindingValueIsValid(stringValue, existingValue, modelProperty, bindingContext)) - { - try - { - BindValue(modelProperty, stringValue, bindingContext); - } - catch (PropertyBindingException ex) - { - bindingExceptions.Add(ex); - } - } + bindingExceptions.Add(ex); } } + else if (modelProperty.PropertyType.IsCollectionOrArrayOrEnumerable()) + { + this.BindValueAsList(blackList, bindingContext, modelProperty, existingValue, bindingExceptions); + } + } + } - if (bindingExceptions.Any() && !bindingContext.Configuration.IgnoreErrors) + private void BindValueAsList(string[] blackList, BindingContext bindingContext, + BindingMemberInfo modelProperty, object existingValue, List bindingExceptions) + { + var pattern = new Regex(modelProperty.Name + @"\[(?\d+)\]\.", RegexOptions.IgnoreCase); + var indexes = bindingContext.RequestData.Keys + .Select(x => pattern.Match(x)) + .Where(x => x.Success) + .Select(x => int.Parse(x.Groups["index"].Value)) + .Distinct() + .ToList(); + if (indexes.Any()) + { + var genericType = GetGenericTypeForModelIfList(modelProperty.PropertyType); + var list = (IList)CreateModel(modelProperty.PropertyType, genericType, existingValue); + var members = GetBindingMembers(genericType, null, blackList).ToList(); + foreach (var i in indexes) { - throw new ModelBindingException(modelType, bindingExceptions); + this.AddInstance(list, i, bindingContext, bindingExceptions, genericType, members, + propName => bindingContext.RequestData.GetValueOrDefault(modelProperty.Name + "[" + i + "]." + propName) ?? string.Empty); } + var value = modelProperty.PropertyType.IsArray() + ? ToArrayMethodInfo.MakeGenericMethod(new[] { genericType }).Invoke(null, new object[] { list}) + : list; + SetBindingMemberValue(modelProperty, bindingContext.Model, value); } + } - if (modelType.IsArray()) + private void AddInstance(IList list, int index, BindingContext bindingContext, List bindingExceptions, Type genericType, IEnumerable validModelBindingMembers, Func getValue) + { + object genericinstance; + if (list.Count > index) { - var generictoArrayMethod = ToArrayMethodInfo.MakeGenericMethod(new[] { genericType }); - return generictoArrayMethod.Invoke(null, new[] { bindingContext.Model }); + genericinstance = list[index]; + } + else + { + genericinstance = genericType.CreateInstance(); + list.Add(genericinstance); + } + + foreach (var modelProperty in validModelBindingMembers) + { + var existingCollectionValue = modelProperty.GetValue(genericinstance); + + var collectionStringValue = getValue(modelProperty.Name); + + if (this.BindingValueIsValid(collectionStringValue, existingCollectionValue, modelProperty,bindingContext)) + { + try + { + BindValue(modelProperty, collectionStringValue, bindingContext, genericinstance); + } + catch (PropertyBindingException ex) + { + bindingExceptions.Add(ex); + } + } } - return bindingContext.Model; } private bool BindingValueIsValid(string bindingValue, object existingValue, BindingMemberInfo modelProperty, BindingContext bindingContext) @@ -207,16 +236,9 @@ private bool BindingValueIsValid(string bindingValue, object existingValue, Bind /// /// Current Context /// An int containing the number of elements - private int GetBindingListInstanceCount(NancyContext context) + private int GetBindingListInstanceCount(BindingContext context) { - var dictionary = context.Request.Form as IDictionary; - - if (dictionary == null) - { - return 0; - } - - return dictionary.Keys.Select(IsMatch).Where(x => x != -1).Distinct().ToArray().Length; + return context.RequestData.Keys.Select(IsMatch).Where(x => x != -1).Distinct().Count(); } private static int IsMatch(string item) @@ -247,8 +269,7 @@ private static void UpdateModelWithDeserializedModel(object bodyDeserializedMode return; } - if (bodyDeserializedModelType.IsCollection() || bodyDeserializedModelType.IsEnumerable() || - bodyDeserializedModelType.IsArray()) + if (bodyDeserializedModelType.IsCollectionOrArrayOrEnumerable()) { var count = 0; @@ -333,19 +354,48 @@ private static bool IsDefaultValue(object existingValue, Type propertyType) : existingValue == null; } - private BindingContext CreateBindingContext(NancyContext context, Type modelType, object instance, BindingConfig configuration, IEnumerable blackList, Type genericType) + private BindingContext CreateBindingContext(NancyContext context, Type modelType, object instance, BindingConfig configuration, string[] blackList) { - return new BindingContext - { - Configuration = configuration, - Context = context, - DestinationType = modelType, - Model = CreateModel(modelType, genericType, instance), - ValidModelBindingMembers = GetBindingMembers(modelType, genericType, blackList).ToList(), - RequestData = this.GetDataFields(context), - GenericType = genericType, - TypeConverters = this.typeConverters.Concat(this.defaults.DefaultTypeConverters), - }; + var genericType = GetGenericTypeForModelIfList(modelType); + + return + new BindingContext + { + Configuration = configuration, + Context = context, + DestinationType = modelType, + Model = CreateModel(modelType, genericType, instance), + ValidModelBindingMembers = GetBindingMembers(modelType, genericType, blackList).ToList(), + RequestData = this.GetDataFields(context), + GenericType = genericType, + TypeConverters = this.typeConverters.Concat(this.defaults.DefaultTypeConverters), + }; + } + + private static Type GetGenericTypeForModelIfList(Type modelType) + { + Type genericType = null; + if (modelType.IsCollectionOrArrayOrEnumerable()) + { + //make sure it has a generic type + if (modelType.GetTypeInfo().IsGenericType) + { + genericType = modelType.GetGenericArguments().FirstOrDefault(); + } + else + { + var ienumerable = + modelType.GetInterfaces().Where(i => i.GetTypeInfo().IsGenericType).FirstOrDefault( + i => i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + genericType = ienumerable == null ? null : ienumerable.GetGenericArguments().FirstOrDefault(); + } + + if (genericType == null) + { + throw new ArgumentException("When modelType is an enumerable it must specify the type.", "modelType"); + } + } + return genericType; } private IDictionary GetDataFields(NancyContext context) @@ -417,7 +467,7 @@ private static IEnumerable GetBindingMembers(Type modelType, private static object CreateModel(Type modelType, Type genericType, object instance) { - if (modelType.IsArray() || modelType.IsCollection() || modelType.IsEnumerable()) + if (modelType.IsCollectionOrArrayOrEnumerable()) { //make sure instance has a Add method. Otherwise call `.ToList` if (instance != null && modelType.IsInstanceOfType(instance)) @@ -446,35 +496,30 @@ private static object CreateModel(Type modelType, Type genericType, object insta instance; } - private static string GetValue(string propertyName, BindingContext context, int index = -1) + private static string GetValue(string propertyName, BindingContext context, int index) { - if (index != -1) + var indexindexes = context.RequestData.Keys.Select(IsMatch) + .Where(i => i != -1) + .OrderBy(i => i) + .Distinct() + .Select((k, i) => new KeyValuePair(i, k)) + .ToDictionary(k => k.Key, v => v.Value); + + if (indexindexes.ContainsKey(index)) { + var propertyValue = + context.RequestData.Where(c => + { + var indexId = IsMatch(c.Key); + return c.Key.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase) && indexId != -1 && indexId == indexindexes[index]; + }) + .Select(k => k.Value) + .FirstOrDefault(); - var indexindexes = context.RequestData.Keys.Select(IsMatch) - .Where(i => i != -1) - .OrderBy(i => i) - .Distinct() - .Select((k, i) => new KeyValuePair(i, k)) - .ToDictionary(k => k.Key, v => v.Value); - - if (indexindexes.ContainsKey(index)) - { - var propertyValue = - context.RequestData.Where(c => - { - var indexId = IsMatch(c.Key); - return c.Key.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase) && indexId != -1 && indexId == indexindexes[index]; - }) - .Select(k => k.Value) - .FirstOrDefault(); - - return propertyValue ?? string.Empty; - } - - return string.Empty; + return propertyValue ?? string.Empty; } - return context.RequestData.ContainsKey(propertyName) ? context.RequestData[propertyName] : string.Empty; + + return string.Empty; } private object DeserializeRequestBody(BindingContext context) diff --git a/src/Nancy/ModelBinding/DefaultConverters/CollectionConverter.cs b/src/Nancy/ModelBinding/DefaultConverters/CollectionConverter.cs index cff264d952..f7ae457b98 100644 --- a/src/Nancy/ModelBinding/DefaultConverters/CollectionConverter.cs +++ b/src/Nancy/ModelBinding/DefaultConverters/CollectionConverter.cs @@ -23,7 +23,7 @@ public class CollectionConverter : ITypeConverter /// True if conversion supported, false otherwise public bool CanConvertTo(Type destinationType, BindingContext context) { - return destinationType.IsCollection() || destinationType.IsEnumerable() || destinationType.IsArray(); + return destinationType.IsCollectionOrArrayOrEnumerable(); } /// diff --git a/test/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs b/test/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs index 16f4bfeed0..161879120a 100644 --- a/test/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs +++ b/test/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs @@ -395,7 +395,7 @@ public void Should_ignore_indexer_properties() binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); // Then - validProperties.ShouldEqual(22); + validProperties.ShouldEqual(BindingMemberInfo.Collect(typeof(TestModel)).Count()); } [Fact] @@ -1513,6 +1513,58 @@ public void Should_bind_to_model_with_non_public_default_constructor() result.IntProperty.ShouldEqual(10); } + [Fact] + public void Should_bind_multiple_Form_properties_to_a_list_as_a_complex_property() + { + //Given + var typeConverters = new ITypeConverter[] { new CollectionConverter(), new FallbackConverter() }; + var binder = this.GetBinder(typeConverters); + + var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); + context.Request.Form["StringProperty"] = "Test"; + context.Request.Form["IntProperty"] = "1"; + context.Request.Form["Friends[0].Name"] = "Steven"; + context.Request.Form["Friends[0].Age"] = "21"; + context.Request.Form["Friends[1].Name"] = "Andreas"; + + // When + var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); + + // Then + result.StringProperty.ShouldEqual("Test"); + result.IntProperty.ShouldEqual(1); + result.Friends.First().Name.ShouldEqual("Steven"); + result.Friends.First().Age.ShouldEqual(21); + result.Friends.Last().Name.ShouldEqual("Andreas"); + result.Friends.Last().Age.ShouldEqual(default(int)); + } + + [Fact] + public void Should_bind_multiple_Form_properties_to_a_array_as_a_complex_property() + { + //Given + var typeConverters = new ITypeConverter[] { new CollectionConverter(), new FallbackConverter() }; + var binder = this.GetBinder(typeConverters); + + var context = CreateContextWithHeader("Content-Type", new[] { "application/x-www-form-urlencoded" }); + context.Request.Form["StringProperty"] = "Test"; + context.Request.Form["IntProperty"] = "1"; + context.Request.Form["FriendsArray[0].Name"] = "Steven"; + context.Request.Form["FriendsArray[0].Age"] = "21"; + context.Request.Form["FriendsArray[1].Name"] = "Andreas"; + + // When + var result = (TestModel)binder.Bind(context, typeof(TestModel), null, BindingConfig.Default); + + // Then + result.StringProperty.ShouldEqual("Test"); + result.IntProperty.ShouldEqual(1); + result.FriendsArray.First().Name.ShouldEqual("Steven"); + result.FriendsArray.First().Age.ShouldEqual(21); + result.FriendsArray.Last().Name.ShouldEqual("Andreas"); + result.FriendsArray.Last().Age.ShouldEqual(default(int)); + } + private IBinder GetBinder(IEnumerable typeConverters = null, IEnumerable bodyDeserializers = null, IFieldNameConverter nameConverter = null, BindingDefaults bindingDefaults = null) { var converters = typeConverters ?? new ITypeConverter[] { new DateTimeConverter(), new NumericConverter(), new FallbackConverter() }; @@ -1623,6 +1675,16 @@ public int this[int index] public List ModelsProperty { get; set; } public List ModelsField; + public List Friends { get; set; } + + public Friend[] FriendsArray { get; set; } + } + + public class Friend + { + public string Name { get; set; } + + public int Age { get; set; } } public class InheritedTestModel : TestModel