diff --git a/src/Nancy.Serialization.JsonNet/JsonNetBodyDeserializer.cs b/src/Nancy.Serialization.JsonNet/JsonNetBodyDeserializer.cs index 12b053c..fcf9717 100644 --- a/src/Nancy.Serialization.JsonNet/JsonNetBodyDeserializer.cs +++ b/src/Nancy.Serialization.JsonNet/JsonNetBodyDeserializer.cs @@ -2,6 +2,8 @@ { using System; using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -14,6 +16,12 @@ public class JsonNetBodyDeserializer : IBodyDeserializer { private readonly JsonSerializer serializer; + private static MethodInfo arrayBuilderMethodDefinition = typeof(JsonNetBodyDeserializer) + .GetMethods(BindingFlags.Static | BindingFlags.NonPublic) + .Single(method => method.Name == "ArrayBuilder"); + + private static readonly ConcurrentDictionary> arrayBuilders = new ConcurrentDictionary>(); + /// /// Empty constructor if no converters are needed /// @@ -75,6 +83,29 @@ public object Deserialize(MediaRange mediaRange, Stream bodyStream, BindingConte return deserializedObject; } + private static object ArrayBuilder(object items, BindingContext context) + { + var returnCollection = (List)ConvertCollection(items, typeof(List), context); + + return returnCollection.ToArray(); + } + + private static Func GetArrayBuilder(Type destinationType) + { + return arrayBuilders.GetOrAdd( + destinationType.GetElementType(), + (elementType) => arrayBuilderMethodDefinition + .MakeGenericMethod(elementType) + .CreateDelegate(typeof(Func)) as Func); + } + + private static object ConvertArray(object items, Type destinationType, BindingContext context) + { + var builder = GetArrayBuilder(destinationType); + + return builder(items, context); + } + private static object ConvertCollection(object items, Type destinationType, BindingContext context) { var returnCollection = Activator.CreateInstance(destinationType); @@ -92,13 +123,18 @@ private static object ConvertCollection(object items, Type destinationType, Bind private static object CreateObjectWithBlacklistExcluded(BindingContext context, object deserializedObject) { - var returnObject = Activator.CreateInstance(context.DestinationType, true); + if (context.DestinationType.IsArray()) + { + return ConvertArray(deserializedObject, context.DestinationType, context); + } if (context.DestinationType.IsCollection()) { return ConvertCollection(deserializedObject, context.DestinationType, context); } + var returnObject = Activator.CreateInstance(context.DestinationType, true); + foreach (var property in context.ValidModelBindingMembers) { CopyPropertyValue(property, deserializedObject, returnObject); @@ -112,4 +148,4 @@ private static void CopyPropertyValue(BindingMemberInfo property, object sourceO property.SetValue(destinationObject, property.GetValue(sourceObject)); } } -} \ No newline at end of file +} diff --git a/test/Nancy.Serialization.JsonNet.Tests/JsonNetBodyDeserializerFixture.cs b/test/Nancy.Serialization.JsonNet.Tests/JsonNetBodyDeserializerFixture.cs index 80a2f8d..ed1d41a 100644 --- a/test/Nancy.Serialization.JsonNet.Tests/JsonNetBodyDeserializerFixture.cs +++ b/test/Nancy.Serialization.JsonNet.Tests/JsonNetBodyDeserializerFixture.cs @@ -1,93 +1,184 @@ -namespace Nancy.Serialization.JsonNet.Tests -{ - using System; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Text; - using Nancy.ModelBinding; - using Newtonsoft.Json; - using Xunit; - - public class JsonNetBodyDeserializerFixture - { - public class TestData - { - private TestData() - { - } - - public TestData(string randomStuff) - { - // Should never get here as it should use the NonPublicDefaultConstructor first. - if (randomStuff == null) - throw new ArgumentNullException("randomStuff"); - } - - public string SomeString { get; set; } - - public Guid SomeGuid { get; set; } - } - - [Fact] - public void when_deserializing() - { - // Given - JsonConvert.DefaultSettings = JsonNetSerializerFixture.GetJsonSerializerSettings; - - var guid = Guid.NewGuid(); - string source = string.Format("{{\"someString\":\"some string value\",\"someGuid\":\"{0}\"}}", guid); - - var context = new BindingContext - { - DestinationType = typeof (TestData), - ValidModelBindingMembers = typeof (TestData).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), - }; - - // When - object actual; - using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) - { - IBodyDeserializer sut = new JsonNetBodyDeserializer(); - actual = sut.Deserialize("application/json", bodyStream, context); - } - - // Then - var actualData = Assert.IsType(actual); - Assert.Equal("some string value", actualData.SomeString); - Assert.Equal(guid, actualData.SomeGuid); - } - - [Fact] - public void when_deserializing_while_the_body_stream_was_not_at_position_zero() - { - // Repro of https://github.com/NancyFx/Nancy.Serialization.JsonNet/issues/22 - - // Given - JsonConvert.DefaultSettings = JsonNetSerializerFixture.GetJsonSerializerSettings; - - var guid = Guid.NewGuid(); - string source = string.Format("{{\"someString\":\"some string value\",\"someGuid\":\"{0}\"}}", guid); - - var context = new BindingContext - { - DestinationType = typeof(TestData), - ValidModelBindingMembers = typeof(TestData).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), - }; - - // When - object actual; - using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) - { - IBodyDeserializer sut = new JsonNetBodyDeserializer(); - bodyStream.Position = 1; - actual = sut.Deserialize("application/json", bodyStream, context); - } - - // Then - var actualData = Assert.IsType(actual); - Assert.Equal("some string value", actualData.SomeString); - Assert.Equal(guid, actualData.SomeGuid); - } - } -} +namespace Nancy.Serialization.JsonNet.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using Nancy.ModelBinding; + using Newtonsoft.Json; + using Xunit; + + public class JsonNetBodyDeserializerFixture + { + public class TestData + { + private TestData() + { + } + + public TestData(string randomStuff) + { + // Should never get here as it should use the NonPublicDefaultConstructor first. + if (randomStuff == null) + throw new ArgumentNullException("randomStuff"); + } + + public string SomeString { get; set; } + + public Guid SomeGuid { get; set; } + } + + [Fact] + public void when_deserializing() + { + // Given + JsonConvert.DefaultSettings = JsonNetSerializerFixture.GetJsonSerializerSettings; + + var guid = Guid.NewGuid(); + string source = string.Format("{{\"someString\":\"some string value\",\"someGuid\":\"{0}\"}}", guid); + + var context = new BindingContext + { + DestinationType = typeof (TestData), + ValidModelBindingMembers = typeof (TestData).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), + }; + + // When + object actual; + using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) + { + IBodyDeserializer sut = new JsonNetBodyDeserializer(); + actual = sut.Deserialize("application/json", bodyStream, context); + } + + // Then + var actualData = Assert.IsType(actual); + Assert.Equal("some string value", actualData.SomeString); + Assert.Equal(guid, actualData.SomeGuid); + } + + [Fact] + public void when_deserializing_while_the_body_stream_was_not_at_position_zero() + { + // Repro of https://github.com/NancyFx/Nancy.Serialization.JsonNet/issues/22 + + // Given + JsonConvert.DefaultSettings = JsonNetSerializerFixture.GetJsonSerializerSettings; + + var guid = Guid.NewGuid(); + string source = string.Format("{{\"someString\":\"some string value\",\"someGuid\":\"{0}\"}}", guid); + + var context = new BindingContext + { + DestinationType = typeof(TestData), + ValidModelBindingMembers = typeof(TestData).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), + }; + + // When + object actual; + using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) + { + IBodyDeserializer sut = new JsonNetBodyDeserializer(); + bodyStream.Position = 1; + actual = sut.Deserialize("application/json", bodyStream, context); + } + + // Then + var actualData = Assert.IsType(actual); + Assert.Equal("some string value", actualData.SomeString); + Assert.Equal(guid, actualData.SomeGuid); + } + + [Fact] + public void when_deserializing_directly_to_array_of_string() + { + // Given + string source = "['first', 'second', 'third']"; + + var context = new BindingContext + { + DestinationType = typeof(string[]), + ValidModelBindingMembers = new BindingMemberInfo[0], + }; + + // When + object actual; + using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) + { + IBodyDeserializer sut = new JsonNetBodyDeserializer(); + actual = sut.Deserialize("application/json", bodyStream, context); + } + + // Then + var actualData = Assert.IsType(actual); + Assert.Equal(3, actualData.Length); + Assert.Equal("first", actualData[0]); + Assert.Equal("second", actualData[1]); + Assert.Equal("third", actualData[2]); + } + + [Fact] + public void when_deserializing_directly_to_array_of_tuple() + { + // Given + string source = "[{'Item1': 'first', 'Item2': 'second'}, {'Item1': 'third', 'Item2': 'fourth'}]"; + + var context = new BindingContext + { + DestinationType = typeof(Tuple[]), + ValidModelBindingMembers = typeof(Tuple).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), + }; + + // When + object actual; + using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) + { + IBodyDeserializer sut = new JsonNetBodyDeserializer(); + actual = sut.Deserialize("application/json", bodyStream, context); + } + + // Then + var actualData = Assert.IsType[]>(actual); + Assert.Equal(2, actualData.Length); + Assert.Equal("first", actualData[0].Item1); + Assert.Equal("second", actualData[0].Item2); + Assert.Equal("third", actualData[1].Item1); + Assert.Equal("fourth", actualData[1].Item2); + } + + [Fact] + public void when_deserializing_directly_to_array_of_pod_type() + { + // Given + string source = "[{'someString': 'first', 'someGuid': ''}, {'someString': 'second', 'someGuid': ''}]"; + + source = System.Text.RegularExpressions.Regex.Replace( + source, + @"\", + (match) => Guid.NewGuid().ToString()); + + var context = new BindingContext + { + DestinationType = typeof(TestData[]), + ValidModelBindingMembers = typeof(TestData).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => new BindingMemberInfo(p)), + }; + + // When + object actual; + using (var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(source))) + { + IBodyDeserializer sut = new JsonNetBodyDeserializer(); + actual = sut.Deserialize("application/json", bodyStream, context); + } + + // Then + var actualData = Assert.IsType(actual); + Assert.Equal(2, actualData.Length); + Assert.Equal("first", actualData[0].SomeString); + Assert.NotEqual(Guid.Empty, actualData[0].SomeGuid); + Assert.Equal("second", actualData[1].SomeString); + Assert.NotEqual(Guid.Empty, actualData[1].SomeGuid); + } + } +}