From cee73c0af32f75896afa8fa4d71a21f4f33130ba Mon Sep 17 00:00:00 2001 From: sornaks Date: Tue, 1 Jul 2014 17:11:49 -0700 Subject: [PATCH] 1. Introducing XML Input Formatters. 2. Adding DelegatingStream class 3. Unit + Functional tests for formatters. --- Mvc.sln | 13 + src/Microsoft.AspNet.Mvc.Common/Encodings.cs | 5 +- .../Formatters/JsonOutputFormatter.cs | 2 +- src/Microsoft.AspNet.Mvc.Core/project.json | 11 +- .../Formatters/DelegatingStream.cs | 189 +++++++++++ .../Formatters/FormattingUtilities.cs | 68 ++++ ...XmlDataContractSerializerInputFormatter.cs | 136 ++++++++ .../Formatters/XmlSerializerInputFormatter.cs | 136 ++++++++ .../Microsoft.AspNet.Mvc.ModelBinding.kproj | 4 + .../project.json | 6 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 2 + .../InputFormatterTests.cs | 43 +++ ...Microsoft.AspNet.Mvc.FunctionalTests.kproj | 1 + .../project.json | 1 + .../Formatters/DelegatingStreamTests.cs | 41 +++ ...taContractSerializerInputFormatterTests.cs | 313 +++++++++++++++++ .../XmlSerializerInputFormatterTests.cs | 317 ++++++++++++++++++ ...crosoft.AspNet.Mvc.ModelBinding.Test.kproj | 3 + .../project.json | 8 +- .../Controllers/HomeController.cs | 16 + .../FormatterWebSite/FormatterWebSite.kproj | 39 +++ .../FormatterWebSite/Models/DummyClass.cs | 10 + test/WebSites/FormatterWebSite/Startup.cs | 32 ++ test/WebSites/FormatterWebSite/project.json | 10 + 24 files changed, 1395 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/DelegatingStream.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/DelegatingStreamTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs create mode 100644 test/WebSites/FormatterWebSite/Controllers/HomeController.cs create mode 100644 test/WebSites/FormatterWebSite/FormatterWebSite.kproj create mode 100644 test/WebSites/FormatterWebSite/Models/DummyClass.cs create mode 100644 test/WebSites/FormatterWebSite/Startup.cs create mode 100644 test/WebSites/FormatterWebSite/project.json diff --git a/Mvc.sln b/Mvc.sln index dad54ecc62..636e42cb3c 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -54,6 +54,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CompositeViewEngine", "test EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorWebSite", "test\WebSites\RazorWebSite\RazorWebSite.kproj", "{B07CAF59-11ED-40E3-A5DB-E1178F84FA78}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatterWebSite", "test\WebSites\FormatterWebSite\FormatterWebSite.kproj", "{62735776-46FF-4170-9392-02E128A69B89}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ValueProvidersSite", "test\WebSites\ValueProvidersSite\ValueProvidersSite.kproj", "{14F79E79-AE79-48FA-95DE-D794EF4EABB3}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.HeaderValueAbstractions", "src\Microsoft.AspNet.Mvc.HeaderValueAbstractions\Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj", "{98335B23-E4B9-4CAD-9749-0DED32A659A1}" EndProject @@ -279,6 +281,16 @@ Global {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|x86.ActiveCfg = Release|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Debug|x86.ActiveCfg = Debug|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Release|Any CPU.Build.0 = Release|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {62735776-46FF-4170-9392-02E128A69B89}.Release|x86.ActiveCfg = Release|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -336,6 +348,7 @@ Global {5F945B82-FE5F-425C-956C-8BC2F2020254} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {A853B2BA-4449-4908-A416-5A3C027FC22B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {B07CAF59-11ED-40E3-A5DB-E1178F84FA78} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {62735776-46FF-4170-9392-02E128A69B89} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {14F79E79-AE79-48FA-95DE-D794EF4EABB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {98335B23-E4B9-4CAD-9749-0DED32A659A1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {E69FD235-2042-43A4-9970-59CB29955B4E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} diff --git a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs index 714a78cd4f..5ab65d219a 100644 --- a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs +++ b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs @@ -16,8 +16,7 @@ namespace Microsoft.AspNet.Mvc /// /// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes. /// - public static readonly Encoding UnicodeEncodingWithBOM = new UnicodeEncoding(bigEndian: false, - byteOrderMark: true, - throwOnInvalidBytes: true); + public static readonly Encoding UTF16EncodingLittleEndian + = new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs index 238ff7e86c..80f443b5f5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Mvc _settings = settings; _indent = indent; SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); - SupportedEncodings.Add(Encodings.UnicodeEncodingWithBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json")); } diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 45e48450fe..9bd0450c0e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -18,7 +18,11 @@ "Newtonsoft.Json": "5.0.8" }, "frameworks": { - "net45": {}, + "net45": { + "dependencies": { + "System.Xml": "" + } + }, "k10": { "dependencies": { "Microsoft.CSharp": "4.0.0.0", @@ -41,13 +45,16 @@ "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.InteropServices": "4.0.20.0", + "System.Runtime.Serialization.Xml": "4.0.0.0", "System.Security.Claims": "1.0.0-*", "System.Security.Cryptography.Encryption": "4.0.0.0", "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0", "System.Security.Principal": "4.0.0.0", "System.Text.Encoding": "4.0.20.0", "System.Threading": "4.0.0.0", - "System.Threading.Tasks": "4.0.10.0" + "System.Threading.Tasks": "4.0.10.0", + "System.Xml.ReaderWriter": "4.0.0.0", + "System.Xml.XmlSerializer": "4.0.0.0" } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/DelegatingStream.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/DelegatingStream.cs new file mode 100644 index 0000000000..d807799f27 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/DelegatingStream.cs @@ -0,0 +1,189 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Stream that delegates to an inner stream. + /// This Stream is present so that the inner stream is not closed + /// even when Close() or Dispose() is called. + /// + public class DelegatingStream : Stream + { + private readonly Stream _innerStream; + + /// + /// Initializes a new object of DelegatingStream + /// + /// The stream on which should not be closed + public DelegatingStream([NotNull] Stream innerStream) + { + _innerStream = innerStream; + } + + protected Stream InnerStream + { + get { return _innerStream; } + } + + /// + public override bool CanRead + { + get { return _innerStream.CanRead; } + } + + /// + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + /// + public override bool CanWrite + { + get { return _innerStream.CanWrite; } + } + + /// + public override long Length + { + get { return _innerStream.Length; } + } + + /// + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + /// + public override int ReadTimeout + { + get { return _innerStream.ReadTimeout; } + set { _innerStream.ReadTimeout = value; } + } + + /// + public override bool CanTimeout + { + get { return _innerStream.CanTimeout; } + } + + /// + public override int WriteTimeout + { + get { return _innerStream.WriteTimeout; } + set { _innerStream.WriteTimeout = value; } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + return _innerStream.Read(buffer, offset, count); + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + } +#if NET45 + /// + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + AsyncCallback callback, object state) + { + return _innerStream.BeginRead(buffer, offset, count, callback, state); + } + + /// + public override int EndRead(IAsyncResult asyncResult) + { + return _innerStream.EndRead(asyncResult); + } +#endif + /// + public override int ReadByte() + { + return _innerStream.ReadByte(); + } + + /// + public override void Flush() + { + _innerStream.Flush(); + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _innerStream.FlushAsync(cancellationToken); + } + + /// + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + _innerStream.Write(buffer, offset, count); + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } +#if NET45 + /// + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + AsyncCallback callback, object state) + { + return _innerStream.BeginWrite(buffer, offset, count, callback, state); + } + + /// + public override void EndWrite(IAsyncResult asyncResult) + { + _innerStream.EndWrite(asyncResult); + } +#endif + /// + public override void WriteByte(byte value) + { + _innerStream.WriteByte(value); + } +#if NET45 + /// + public override void Close() + { + } +#endif + /// + protected override void Dispose(bool disposing) + { + // No-op. In CoreCLR this is equivalent to Close. + // Given that we don't own the underlying stream, we never want to do anything interesting here. + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs new file mode 100644 index 0000000000..9edcb396e6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs @@ -0,0 +1,68 @@ +// 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 System.Text; +using System.Xml; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Contains methods which are used by input formatters. + /// + public static class FormattingUtilities + { + public static readonly int DefaultMaxDepth = 32; + + /// + /// Gets the default Reader Quotas for XmlReader. + /// + /// XmlReaderQuotas with default values + public static XmlDictionaryReaderQuotas GetDefaultXmlReaderQuotas() + { +#if NET45 + return new XmlDictionaryReaderQuotas() + { + MaxArrayLength = Int32.MaxValue, + MaxBytesPerRead = Int32.MaxValue, + MaxDepth = DefaultMaxDepth, + MaxNameTableCharCount = Int32.MaxValue, + MaxStringContentLength = Int32.MaxValue + }; +#else + return XmlDictionaryReaderQuotas.Max; +#endif + } + + /// Internal because ContentTypeHeaderValue is internal. + internal static Encoding SelectCharacterEncoding(IList supportedEncodings, + ContentTypeHeaderValue contentType, Type callerType) + { + if (contentType != null) + { + // Find encoding based on content type charset parameter + var charset = contentType.CharSet; + if (!string.IsNullOrWhiteSpace(contentType.CharSet)) + { + for (var i = 0; i < supportedEncodings.Count; i++) + { + var supportedEncoding = supportedEncodings[i]; + if (string.Equals(charset, supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) + { + return supportedEncoding; + } + } + } + } + + if (supportedEncodings.Count > 0) + { + return supportedEncodings[0]; + } + + // No supported encoding was found so there is no way for us to start reading. + throw new InvalidOperationException(Resources.FormatMediaTypeFormatterNoEncoding(callerType.FullName)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs new file mode 100644 index 0000000000..a8a06c197f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs @@ -0,0 +1,136 @@ +// 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 System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// This class handles deserialization of input XML data + /// to strongly-typed objects using . + /// + public class XmlDataContractSerializerInputFormatter : IInputFormatter + { + private readonly IList _supportedEncodings; + private readonly IList _supportedMediaTypes; + private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); + + /// + /// Initializes a new instance of DataContractSerializerInputFormatter + /// + public XmlDataContractSerializerInputFormatter() + { + _supportedMediaTypes = new List + { + "application/xml", + "text/xml" + }; + + _supportedEncodings = new List + { + Encodings.UTF8EncodingWithoutBOM, + Encodings.UTF16EncodingLittleEndian + }; + } + + /// + /// Returns the list of supported encodings. + /// + public IList SupportedEncodings + { + get { return _supportedEncodings; } + } + + /// + /// Returns the list of supported Media Types. + /// + public IList SupportedMediaTypes + { + get { return _supportedMediaTypes; } + } + + /// + /// Indicates the acceptable input XML depth. + /// + public int MaxDepth + { + get { return _readerQuotas.MaxDepth; } + set { _readerQuotas.MaxDepth = value; } + } + + /// + /// The quotas include - DefaultMaxDepth, DefaultMaxStringContentLength, DefaultMaxArrayLength, + /// DefaultMaxBytesPerRead, DefaultMaxNameTableCharCount + /// + public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas + { + get { return _readerQuotas; } + } + + /// + /// Reads the input XML. + /// + /// The input formatter context which contains the body to be read. + /// Task which reads the input. + public async Task ReadAsync(InputFormatterContext context) + { + var request = context.HttpContext.Request; + if (request.ContentLength == 0) + { + context.Model = GetDefaultValueForType(context.Metadata.ModelType); + return; + } + + context.Model = await ReadInternal(context); + } + + /// + /// Called during deserialization to get the . + /// + /// The from which to read. + /// The used during deserialization. + protected virtual XmlReader CreateXmlReader([NotNull] Stream readStream) + { + return XmlDictionaryReader.CreateTextReader( + readStream, _readerQuotas); + } + + /// + /// Called during deserialization to get the . + /// + /// The used during deserialization. + protected virtual XmlObjectSerializer CreateDataContractSerializer(Type type) + { + return new DataContractSerializer(type); + } + + private object GetDefaultValueForType(Type modelType) + { + if (modelType.GetTypeInfo().IsValueType) + { + return Activator.CreateInstance(modelType); + } + + return null; + } + + private Task ReadInternal(InputFormatterContext context) + { + var type = context.Metadata.ModelType; + var request = context.HttpContext.Request; + + using (var xmlReader = CreateXmlReader(new DelegatingStream(request.Body))) + { + var xmlSerializer = CreateDataContractSerializer(type); + return Task.FromResult(xmlSerializer.ReadObject(xmlReader)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs new file mode 100644 index 0000000000..bbeaac500d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs @@ -0,0 +1,136 @@ +// 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 System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// This class handles deserialization of input XML data + /// to strongly-typed objects using + /// + public class XmlSerializerInputFormatter : IInputFormatter + { + private readonly IList _supportedEncodings; + private readonly IList _supportedMediaTypes; + private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); + + /// + /// Initializes a new instance of XmlSerializerInputFormatter. + /// + public XmlSerializerInputFormatter() + { + _supportedMediaTypes = new List + { + "application/xml", + "text/xml" + }; + + _supportedEncodings = new List + { + Encodings.UTF8EncodingWithoutBOM, + Encodings.UTF16EncodingLittleEndian + }; + } + + /// + /// Returns the list of supported encodings. + /// + public IList SupportedEncodings + { + get { return _supportedEncodings; } + } + + /// + /// Returns the list of supported Media Types. + /// + public IList SupportedMediaTypes + { + get { return _supportedMediaTypes; } + } + + /// + /// Indicates the acceptable input XML depth. + /// + public int MaxDepth + { + get { return _readerQuotas.MaxDepth; } + set { _readerQuotas.MaxDepth = value; } + } + + /// + /// The quotas include - DefaultMaxDepth, DefaultMaxStringContentLength, DefaultMaxArrayLength, + /// DefaultMaxBytesPerRead, DefaultMaxNameTableCharCount + /// + public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas + { + get { return _readerQuotas; } + } + + /// + /// Reads the input XML. + /// + /// The input formatter context which contains the body to be read. + /// Task which reads the input. + public async Task ReadAsync(InputFormatterContext context) + { + var request = context.HttpContext.Request; + if (request.ContentLength == 0) + { + context.Model = GetDefaultValueForType(context.Metadata.ModelType); + return; + } + + context.Model = await ReadInternal(context); + } + + /// + /// Called during deserialization to get the . + /// + /// The from which to read. + /// The used during deserialization. + protected virtual XmlReader CreateXmlReader([NotNull] Stream readStream) + { + return XmlDictionaryReader.CreateTextReader( + readStream, _readerQuotas); + } + + /// + /// Called during deserialization to get the . + /// + /// The used during serialization and deserialization. + protected virtual XmlSerializer CreateXmlSerializer(Type type) + { + return new XmlSerializer(type); + } + + private object GetDefaultValueForType(Type modelType) + { + if (modelType.GetTypeInfo().IsValueType) + { + return Activator.CreateInstance(modelType); + } + + return null; + } + + private Task ReadInternal(InputFormatterContext context) + { + var type = context.Metadata.ModelType; + var request = context.HttpContext.Request; + + using (var xmlReader = CreateXmlReader(new DelegatingStream(request.Body))) + { + var xmlSerializer = CreateXmlSerializer(type); + return Task.FromResult(xmlSerializer.Deserialize(xmlReader)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj index c4181d32c6..9583a0b71a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj @@ -40,12 +40,16 @@ + + + + diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index a515f512b7..e1481b6968 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -13,6 +13,7 @@ "frameworks": { "net45": { "dependencies": { + "System.Xml": "", "System.ComponentModel.DataAnnotations": "", "System.Runtime.Serialization": "" } @@ -40,10 +41,13 @@ "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.InteropServices": "4.0.20.0", "System.Runtime.Serialization.Primitives": "4.0.0.0", + "System.Runtime.Serialization.Xml": "4.0.0.0", "System.Text.Encoding": "4.0.20.0", "System.Text.Encoding.Extensions": "4.0.10.0", "System.Threading": "4.0.0.0", - "System.Threading.Tasks": "4.0.10.0" + "System.Threading.Tasks": "4.0.10.0", + "System.Xml.ReaderWriter": "4.0.0.0", + "System.Xml.XmlSerializer": "4.0.0.0" } } } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 724ea7050d..6e9a6ef834 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -62,6 +62,8 @@ namespace Microsoft.AspNet.Mvc yield return describe.Scoped(); yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Transient(); yield return describe.Transient(); yield return describe.Transient(); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs new file mode 100644 index 0000000000..0dae828d84 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs @@ -0,0 +1,43 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class InputFormatterTests + { + private readonly IServiceProvider _services; + private readonly Action _app = new FormatterWebSite.Startup().Configure; + + public InputFormatterTests() + { + _services = TestHelper.CreateServices("FormatterWebSite"); + } + + [Fact] + public async Task CheckIfXmlInputFormatterIsBeingCalled() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.Handler; + var sampleInputInt = 10; + var input = "" + + "" + sampleInputInt.ToString() + ""; + + // Act + var response = await client.PostAsync("http://localhost/Home/Index", input, "application/xml"); + + //Assert + Assert.Equal(200, response.StatusCode); + Assert.Equal(sampleInputInt.ToString(), await response.ReadBodyAsStringAsync()); + } + + // TODO: By default XmlSerializerInputFormatter is called because of the order in which + // the formatters are registered. Add a test to call into DataContractSerializerInputFormatter. + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj index 9ff61cc4c7..342dae7c28 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj @@ -36,6 +36,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 8c4c09f25a..1a927656d0 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -6,6 +6,7 @@ "ActivatorWebSite": "", "BasicWebSite": "", "CompositeViewEngine": "", + "FormatterWebSite": "", "InlineConstraintsWebSite": "", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "", diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/DelegatingStreamTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/DelegatingStreamTests.cs new file mode 100644 index 0000000000..d19295db4d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/DelegatingStreamTests.cs @@ -0,0 +1,41 @@ +// 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. + +#if NET45 +using System.IO; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class DelegatingStreamTests + { + [Fact] + public void InnerStreamIsOpenOnClose() + { + // Arrange + var innerStream = new MemoryStream(); + var delegatingStream = new DelegatingStream(innerStream); + + // Act + delegatingStream.Close(); + + // Assert + Assert.True(innerStream.CanRead); + } + + [Fact] + public void InnerStreamIsOpenOnDispose() + { + // Arrange + var innerStream = new MemoryStream(); + var delegatingStream = new DelegatingStream(innerStream); + + // Act + delegatingStream.Dispose(); + + // Assert + Assert.True(innerStream.CanRead); + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs new file mode 100644 index 0000000000..cd8494ae14 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs @@ -0,0 +1,313 @@ +// 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. + +#if NET45 +using System; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.AspNet.Http; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataContractSerializerInputFormatterTests + { + [DataContract(Name = "DummyClass", Namespace = "")] + public class DummyClass + { + [DataMember] + public int SampleInt { get; set; } + } + + [DataContract(Name = "TestLevelOne", Namespace = "")] + public class TestLevelOne + { + [DataMember] + public int SampleInt { get; set; } + [DataMember] + public string sampleString; + public DateTime SampleDate { get; set; } + } + + [DataContract(Name = "TestLevelTwo", Namespace = "")] + public class TestLevelTwo + { + [DataMember] + public string SampleString { get; set; } + [DataMember] + public TestLevelOne TestOne { get; set; } + } + + [Fact] + public void XmlDataContractSerializerFormatterHasProperSuppportedMediaTypes() + { + // Arrange & Act + var formatter = new XmlDataContractSerializerInputFormatter(); + + // Assert + Assert.True(formatter.SupportedMediaTypes.Contains("application/xml")); + Assert.True(formatter.SupportedMediaTypes.Contains("text/xml")); + } + + [Fact] + public void XmlDataContractSerializerFormatterHasProperSuppportedEncodings() + { + // Arrange & Act + var formatter = new XmlDataContractSerializerInputFormatter(); + + // Assert + Assert.True(formatter.SupportedEncodings.Any(i => i.WebName == "utf-8")); + Assert.True(formatter.SupportedEncodings.Any(i => i.WebName == "utf-16")); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterReadsSimpleTypes() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + + var input = "" + + "" + expectedInt + "" + + "" + expectedString + ""; + + var formatter = new XmlDataContractSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelOne; + Assert.Equal(expectedInt, model.SampleInt); + Assert.Equal(expectedString, model.sampleString); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterReadsComplexTypes() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + var expectedLevelTwoString = "102"; + + var input = "" + + "" + expectedLevelTwoString + "" + + "" + expectedInt + "" + + "" + expectedString + ""; + + var formatter = new XmlDataContractSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelTwo; + Assert.Equal(expectedLevelTwoString, model.SampleString); + Assert.Equal(expectedInt, model.TestOne.SampleInt); + Assert.Equal(expectedString, model.TestOne.sampleString); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterReadsWhenMaxDepthIsModified() + { + // Arrange + var expectedInt = 10; + + var input = "" + + "" + expectedInt + ""; + var formatter = new XmlDataContractSerializerInputFormatter(); + formatter.MaxDepth = 10; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + var model = context.Model as DummyClass; + Assert.Equal(expectedInt, model.SampleInt); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterThrowsOnExceededMaxDepth() + { + // Arrange + var input = "" + + "test" + + "10" + + "test"; + var formatter = new XmlDataContractSerializerInputFormatter(); + formatter.MaxDepth = 1; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act & Assert + await Assert.ThrowsAsync(typeof(SerializationException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterThrowsWhenReaderQuotasAreChanged() + { + // Arrange + var input = "" + + "test" + + "10" + + "test"; + var formatter = new XmlDataContractSerializerInputFormatter(); + formatter.XmlDictionaryReaderQuotas.MaxStringContentLength = 2; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act & Assert + await Assert.ThrowsAsync(typeof(SerializationException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public void XmlDataContractSerializerFormatterThrowsWhenMaxDepthIsBelowOne() + { + // Arrange + var formatter = new XmlDataContractSerializerInputFormatter(); + + // Act & Assert + Assert.Throws(typeof(ArgumentException), () => formatter.MaxDepth = 0); + } + + [Fact] + public async Task VerifyStreamIsOpenAfterRead() + { + // Arrange + var input = "" + + "10"; + var formatter = new XmlDataContractSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.True(context.HttpContext.Request.Body.CanRead); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterThrowsOnInvalidCharacters() + { + // Arrange + var inpStart = Encodings.UTF16EncodingLittleEndian.GetBytes("" + + ""); + byte[] inp = { 192, 193 }; + var inpEnd = Encodings.UTF16EncodingLittleEndian.GetBytes(""); + + var contentBytes = new byte[inpStart.Length + inp.Length + inpEnd.Length]; + Buffer.BlockCopy(inpStart, 0, contentBytes, 0, inpStart.Length); + Buffer.BlockCopy(inp, 0, contentBytes, inpStart.Length, inp.Length); + Buffer.BlockCopy(inpEnd, 0, contentBytes, inpStart.Length + inp.Length, inpEnd.Length); + + var formatter = new XmlDataContractSerializerInputFormatter(); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await Assert.ThrowsAsync(typeof(XmlException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public async Task XmlDataContractSerializerFormatterIgnoresBOMCharacters() + { + // Arrange + var sampleString = "Test"; + var sampleStringBytes = Encoding.UTF8.GetBytes(sampleString); + var inputStart = Encoding.UTF8.GetBytes("" + Environment.NewLine + + "" + sampleString); + byte[] bom = { 0xef, 0xbb, 0xbf }; + var inputEnd = Encoding.UTF8.GetBytes(""); + var expectedBytes = new byte[sampleString.Length + bom.Length]; + + var contentBytes = new byte[inputStart.Length + bom.Length + inputEnd.Length]; + Buffer.BlockCopy(inputStart, 0, contentBytes, 0, inputStart.Length); + Buffer.BlockCopy(bom, 0, contentBytes, inputStart.Length, bom.Length); + Buffer.BlockCopy(inputEnd, 0, contentBytes, inputStart.Length + bom.Length, inputEnd.Length); + + var formatter = new XmlDataContractSerializerInputFormatter(); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + var model = context.Model as TestLevelTwo; + Buffer.BlockCopy(sampleStringBytes, 0, expectedBytes, 0, sampleStringBytes.Length); + Buffer.BlockCopy(bom, 0, expectedBytes, sampleStringBytes.Length, bom.Length); + Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(model.SampleString)); + } + + [Fact] + public async Task XmlDataContractSerializerAcceptsUTF16Characters() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + + var input = "" + + "" + expectedInt + "" + + "" + expectedString + ""; + + var formatter = new XmlDataContractSerializerInputFormatter(); + var contentBytes = Encodings.UTF16EncodingLittleEndian.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelOne; + Assert.Equal(expectedInt, model.SampleInt); + Assert.Equal(expectedString, model.sampleString); + } + + private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType) + { + var httpContext = GetHttpContext(contentBytes); + var modelState = new ModelStateDictionary(); + var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType); + return new InputFormatterContext(httpContext, metadata, modelState); + } + + private static HttpContext GetHttpContext(byte[] contentBytes, + string contentType = "application/xml") + { + var request = new Mock(); + var headers = new Mock(); + headers.SetupGet(h => h["Content-Type"]).Returns(contentType); + request.SetupGet(r => r.Headers).Returns(headers.Object); + request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes)); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + return httpContext.Object; + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs new file mode 100644 index 0000000000..2b21d1e20f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs @@ -0,0 +1,317 @@ +// 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. + +#if NET45 +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.AspNet.Http; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class XmlSerializerInputFormatterTests + { + public class DummyClass + { + public int SampleInt { get; set; } + } + + public class TestLevelOne + { + public int SampleInt { get; set; } + public string sampleString; + public DateTime SampleDate { get; set; } + } + + public class TestLevelTwo + { + public string SampleString { get; set; } + public TestLevelOne TestOne { get; set; } + } + + [Fact] + public void XmlSerializerFormatterHasProperSuppportedMediaTypes() + { + // Arrange & Act + var formatter = new XmlSerializerInputFormatter(); + + // Assert + Assert.True(formatter.SupportedMediaTypes.Contains("application/xml")); + Assert.True(formatter.SupportedMediaTypes.Contains("text/xml")); + } + + [Fact] + public void XmlSerializerFormatterHasProperSuppportedEncodings() + { + // Arrange & Act + var formatter = new XmlSerializerInputFormatter(); + + // Assert + Assert.True(formatter.SupportedEncodings.Any(i => i.WebName == "utf-8")); + Assert.True(formatter.SupportedEncodings.Any(i => i.WebName == "utf-16")); + } + + [Fact] + public async Task XmlSerializerFormatterReadsSimpleTypes() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + var expectedDateTime = XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc); + + var input = "" + + "" + expectedInt + "" + + "" + expectedString + "" + + "" + expectedDateTime + ""; + + var formatter = new XmlSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelOne; + Assert.Equal(expectedInt, model.SampleInt); + Assert.Equal(expectedString, model.sampleString); + Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate); + } + + [Fact] + public async Task XmlSerializerFormatterReadsComplexTypes() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + var expectedDateTime = XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc); + var expectedLevelTwoString = "102"; + + var input = "" + + "" + expectedLevelTwoString + "" + + "" + expectedInt + "" + + "" + expectedString + "" + + "" + expectedDateTime + ""; + + var formatter = new XmlSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelTwo; + Assert.Equal(expectedLevelTwoString, model.SampleString); + Assert.Equal(expectedInt, model.TestOne.SampleInt); + Assert.Equal(expectedString, model.TestOne.sampleString); + Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.TestOne.SampleDate); + } + + [Fact] + public async Task XmlSerializerFormatterReadsWhenMaxDepthIsModified() + { + // Arrange + var expectedInt = 10; + + var input = "" + + "" + expectedInt + ""; + var formatter = new XmlSerializerInputFormatter(); + formatter.MaxDepth = 10; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + var model = context.Model as DummyClass; + Assert.Equal(expectedInt, model.SampleInt); + } + + [Fact] + public async Task XmlSerializerFormatterThrowsOnExceededMaxDepth() + { + // Arrange + var input = "" + + "test" + + "10" + + "test" + + "" + XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc) + + ""; + var formatter = new XmlSerializerInputFormatter(); + formatter.MaxDepth = 1; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act & Assert + await Assert.ThrowsAsync(typeof(InvalidOperationException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public async Task XmlSerializerFormatterThrowsWhenReaderQuotasAreChanged() + { + // Arrange + var input = "" + + "test" + + "10" + + "test" + + "" + XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc) + + ""; + var formatter = new XmlSerializerInputFormatter(); + formatter.XmlDictionaryReaderQuotas.MaxStringContentLength = 10; + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act & Assert + await Assert.ThrowsAsync(typeof(InvalidOperationException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public void XmlSerializerSerializerThrowsWhenMaxDepthIsBelowOne() + { + // Arrange + var formatter = new XmlSerializerInputFormatter(); + + // Act & Assert + Assert.Throws(typeof(ArgumentException), () => formatter.MaxDepth = 0); + } + + [Fact] + public async Task VerifyStreamIsOpenAfterRead() + { + // Arrange + var input = "" + + "10"; + var formatter = new XmlSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.True(context.HttpContext.Request.Body.CanRead); + } + + [Fact] + public async Task XmlSerializerFormatterThrowsOnInvalidCharacters() + { + // Arrange + var inpStart = Encodings.UTF16EncodingLittleEndian.GetBytes("" + + ""); + byte[] inp = { 192, 193 }; + var inpEnd = Encodings.UTF16EncodingLittleEndian.GetBytes(""); + + var contentBytes = new byte[inpStart.Length + inp.Length + inpEnd.Length]; + Buffer.BlockCopy(inpStart, 0, contentBytes, 0, inpStart.Length); + Buffer.BlockCopy(inp, 0, contentBytes, inpStart.Length, inp.Length); + Buffer.BlockCopy(inpEnd, 0, contentBytes, inpStart.Length + inp.Length, inpEnd.Length); + + var formatter = new XmlSerializerInputFormatter(); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await Assert.ThrowsAsync(typeof(XmlException), async () => await formatter.ReadAsync(context)); + } + + [Fact] + public async Task XmlSerializerFormatterIgnoresBOMCharacters() + { + // Arrange + var sampleString = "Test"; + var sampleStringBytes = Encoding.UTF8.GetBytes(sampleString); + var inputStart = Encoding.UTF8.GetBytes("" + Environment.NewLine + + "" + sampleString); + byte[] bom = { 0xef, 0xbb, 0xbf }; + var inputEnd = Encoding.UTF8.GetBytes(""); + var expectedBytes = new byte[sampleString.Length + bom.Length]; + + var contentBytes = new byte[inputStart.Length + bom.Length + inputEnd.Length]; + Buffer.BlockCopy(inputStart, 0, contentBytes, 0, inputStart.Length); + Buffer.BlockCopy(bom, 0, contentBytes, inputStart.Length, bom.Length); + Buffer.BlockCopy(inputEnd, 0, contentBytes, inputStart.Length + bom.Length, inputEnd.Length); + + var formatter = new XmlSerializerInputFormatter(); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + var model = context.Model as TestLevelTwo; + Buffer.BlockCopy(sampleStringBytes, 0, expectedBytes, 0, sampleStringBytes.Length); + Buffer.BlockCopy(bom, 0, expectedBytes, sampleStringBytes.Length, bom.Length); + Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(model.SampleString)); + } + + [Fact] + public async Task XmlSerializerFormatterAcceptsUTF16Characters() + { + // Arrange + var expectedInt = 10; + var expectedString = "TestString"; + var expectedDateTime = XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc); + + var input = "" + + "" + expectedInt + "" + + "" + expectedString + "" + + "" + expectedDateTime + ""; + + var formatter = new XmlSerializerInputFormatter(); + var contentBytes = Encodings.UTF16EncodingLittleEndian.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne)); + + // Act + await formatter.ReadAsync(context); + + // Assert + Assert.NotNull(context.Model); + Assert.IsType(context.Model); + + var model = context.Model as TestLevelOne; + Assert.Equal(expectedInt, model.SampleInt); + Assert.Equal(expectedString, model.sampleString); + Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate); + } + + private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType) + { + var httpContext = GetHttpContext(contentBytes); + var modelState = new ModelStateDictionary(); + var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType); + return new InputFormatterContext(httpContext, metadata, modelState); + } + + private static HttpContext GetHttpContext(byte[] contentBytes, + string contentType = "application/xml") + { + var request = new Mock(); + var headers = new Mock(); + headers.SetupGet(h => h["Content-Type"]).Returns(contentType); + request.SetupGet(r => r.Headers).Returns(headers.Object); + request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes)); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + return httpContext.Object; + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj index 2d634be717..2c0f04de8f 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj @@ -32,7 +32,10 @@ + + + diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json index 422b84a159..d03b434a9e 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json @@ -10,6 +10,7 @@ "Microsoft.AspNet.Testing": "1.0.0-*", "Microsoft.DataAnnotations" : "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Moq": "4.2.1312.1622", "Newtonsoft.Json": "5.0.8", "Xunit.KRunner": "1.0.0-*" }, @@ -17,9 +18,8 @@ "test": "Xunit.KRunner" }, "frameworks": { - "net45": { - dependencies: { - "Moq": "4.2.1312.1622", + "net45": { + "dependencies": { "System.ComponentModel.DataAnnotations": "", "System.Reflection": "", "System.Runtime": "", @@ -27,7 +27,7 @@ } }, "k10" : { - dependencies: { + "dependencies": { "System.Collections": "4.0.10.0", "System.ComponentModel": "4.0.0.0", "System.Globalization": "4.0.10.0", diff --git a/test/WebSites/FormatterWebSite/Controllers/HomeController.cs b/test/WebSites/FormatterWebSite/Controllers/HomeController.cs new file mode 100644 index 0000000000..873094da3a --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace FormatterWebSite.Controllers +{ + public class HomeController : Controller + { + [HttpPost] + public IActionResult Index([FromBody]DummyClass dummyObject) + { + return Content(dummyObject.SampleInt.ToString()); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/FormatterWebSite.kproj b/test/WebSites/FormatterWebSite/FormatterWebSite.kproj new file mode 100644 index 0000000000..b5b34fbbd0 --- /dev/null +++ b/test/WebSites/FormatterWebSite/FormatterWebSite.kproj @@ -0,0 +1,39 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + Debug + AnyCPU + + + + 62735776-46ff-4170-9392-02e128a69b89 + Web + + + ConsoleDebugger + + + WebDebugger + + + + + 2.0 + + + 38820 + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Models/DummyClass.cs b/test/WebSites/FormatterWebSite/Models/DummyClass.cs new file mode 100644 index 0000000000..a4f603db80 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/DummyClass.cs @@ -0,0 +1,10 @@ +// 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. + +namespace FormatterWebSite +{ + public class DummyClass + { + public int SampleInt { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs new file mode 100644 index 0000000000..26fccfaae7 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; + +namespace FormatterWebSite +{ + public class Startup + { + public void Configure(IBuilder app) + { + var configuration = app.GetTestConfiguration(); + + // Set up application services + app.UseServices(services => + { + // Add MVC services to the services container + services.AddMvc(configuration); + }); + + // Add MVC to the request pipeline + app.UseMvc(routes => + { + routes.MapRoute("ActionAsMethod", "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }); + + }); + } + } +} diff --git a/test/WebSites/FormatterWebSite/project.json b/test/WebSites/FormatterWebSite/project.json new file mode 100644 index 0000000000..7c0d0637ed --- /dev/null +++ b/test/WebSites/FormatterWebSite/project.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Mvc.TestConfiguration": "" + }, + "configurations": { + "net45": { }, + "k10": { } + } +}