From d97a427b3d9d09dea0aaab723a0a5c623130b4a4 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Thu, 31 Jul 2014 18:01:30 -0700 Subject: [PATCH] Adding Input Formatters to MVC Options and using HeaderValueAbstractions --- .../DefaultInputFormattersProvider.cs | 39 +++++++++ .../Formatters/IInputFormattersProvider.cs | 19 +++++ .../Formatters/TempInputFormatterProvider.cs | 52 ++++++++++++ src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs | 4 + .../InputFormatterDescriptor.cs | 34 ++++++++ .../InputFormatterDescriptorExtensions.cs | 84 +++++++++++++++++++ .../MediaTypeWithQualityHeaderValue.cs | 4 +- .../Formatters/FormattingUtilities.cs | 31 +------ .../Formatters/IInputFormatter.cs | 3 +- .../Formatters/JsonInputFormatter.cs | 54 +++++------- .../Formatters/TempInputFormatterProvider.cs | 52 ------------ ...XmlDataContractSerializerInputFormatter.cs | 38 +++------ .../Formatters/XmlSerializerInputFormatter.cs | 40 +++------ .../Internal/ContentTypeHeaderValue.cs | 19 ----- .../Internal/HttpRequestExtensions.cs | 31 ------- .../FormValueProviderFactory.cs | 10 ++- .../project.json | 1 + src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 7 ++ src/Microsoft.AspNet.Mvc/MvcServices.cs | 4 +- .../InputFormatterDescriptorExtensionTest.cs | 73 ++++++++++++++++ .../InputFormatterDescriptorTest.cs | 25 ++++++ .../InputFormatterTests.cs | 17 ++++ .../Formatters/JsonInputFormatterTest.cs | 2 +- ...taContractSerializerInputFormatterTests.cs | 8 +- .../XmlSerializerInputFormatterTests.cs | 8 +- .../FormValueProviderFactoryTests.cs | 5 +- .../MvcOptionSetupTest.cs | 17 ++++ 27 files changed, 440 insertions(+), 241 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormattersProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/IInputFormattersProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/TempInputFormatterProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptorExtensions.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/TempInputFormatterProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ContentTypeHeaderValue.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Internal/HttpRequestExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorExtensionTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormattersProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormattersProvider.cs new file mode 100644 index 0000000000..4c1b0ef72b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormattersProvider.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Mvc.OptionDescriptors +{ + /// + public class DefaultInputFormattersProvider + : OptionDescriptorBasedProvider, IInputFormattersProvider + { + /// + /// Initializes a new instance of the DefaultInputFormattersProvider class. + /// + /// An accessor to the configured for this application. + /// An instance used to instantiate types. + /// A instance that retrieves services from the + /// service collection. + public DefaultInputFormattersProvider(IOptionsAccessor optionsAccessor, + ITypeActivator typeActivator, + IServiceProvider serviceProvider) + : base(optionsAccessor.Options.InputFormatters, typeActivator, serviceProvider) + { + } + + /// + public IReadOnlyList InputFormatters + { + get + { + return Options; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/IInputFormattersProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/IInputFormattersProvider.cs new file mode 100644 index 0000000000..9325f4e114 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/IInputFormattersProvider.cs @@ -0,0 +1,19 @@ +// 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.Collections.Generic; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides an activated collection of instances. + /// + public interface IInputFormattersProvider + { + /// + /// Gets a collection of activated InputFormatter instances. + /// + IReadOnlyList InputFormatters { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/TempInputFormatterProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/TempInputFormatterProvider.cs new file mode 100644 index 0000000000..681e1675ed --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/TempInputFormatterProvider.cs @@ -0,0 +1,52 @@ +// 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.Globalization; +using System.Linq; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Mvc +{ + public class TempInputFormatterProvider : IInputFormatterProvider + { + private IInputFormattersProvider _defaultFormattersProvider; + + public TempInputFormatterProvider([NotNull] IInputFormattersProvider formattersProvider) + { + _defaultFormattersProvider = formattersProvider; + } + + public IInputFormatter GetInputFormatter(InputFormatterProviderContext context) + { + var request = context.HttpContext.Request; + MediaTypeHeaderValue contentType; + if (!MediaTypeHeaderValue.TryParse(request.ContentType, out contentType)) + { + // TODO: https://github.com/aspnet/Mvc/issues/458 + throw new InvalidOperationException("400: Bad Request"); + } + + var formatters = _defaultFormattersProvider.InputFormatters; + foreach (var formatter in formatters) + { + var formatterMatched = formatter.SupportedMediaTypes + .Any(supportedMediaType => + supportedMediaType.IsSubsetOf(contentType)); + if (formatterMatched) + { + return formatter; + } + } + + // TODO: https://github.com/aspnet/Mvc/issues/458 + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "415: Unsupported content type {0}", + contentType.RawValue)); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs index 8861cd4ef9..ba1b511050 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.OptionDescriptors; using Microsoft.AspNet.Mvc.ReflectedModelBuilder; @@ -24,6 +25,7 @@ namespace Microsoft.AspNet.Mvc ViewEngines = new List(); ValueProviderFactories = new List(); OutputFormatters = new List(); + InputFormatters = new List(); } /// @@ -51,6 +53,8 @@ namespace Microsoft.AspNet.Mvc public List OutputFormatters { get; private set; } + public List InputFormatters { get; private set; } + /// /// Provides programmatic configuration for the default . /// diff --git a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptor.cs b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptor.cs new file mode 100644 index 0000000000..1b2ccd5da3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptor.cs @@ -0,0 +1,34 @@ +// 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 Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc.OptionDescriptors +{ + /// + /// Encapsulates information that describes an . + /// + public class InputFormatterDescriptor : OptionDescriptor + { + /// + /// Creates a new instance of . + /// + /// A type that the descriptor represents. + /// + public InputFormatterDescriptor([NotNull] Type type) + : base(type) + { + } + + /// + /// Creates a new instance of . + /// + /// An instance of + /// that the descriptor represents. + public InputFormatterDescriptor([NotNull] IInputFormatter inputFormatter) + : base(inputFormatter) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptorExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptorExtensions.cs new file mode 100644 index 0000000000..8b1902c1e5 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/InputFormatterDescriptorExtensions.cs @@ -0,0 +1,84 @@ +// 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 Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.OptionDescriptors; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Extension methods for adding Input formatters to a collection. + /// + public static class InputFormatterDescriptorExtensions + { + /// + /// Adds a type representing a to a descriptor collection. + /// + /// A list of InputFormatterDescriptors + /// Type representing an . + /// InputFormatterDescriptor representing the added instance. + public static InputFormatterDescriptor Add([NotNull] this IList descriptors, + [NotNull] Type inputFormatterType) + { + var descriptor = new InputFormatterDescriptor(inputFormatterType); + descriptors.Add(descriptor); + return descriptor; + } + + /// + /// Inserts a type representing a to a descriptor collection. + /// + /// A list of InputFormatterDescriptors + /// Type representing an . + /// InputFormatterDescriptor representing the inserted instance. + public static InputFormatterDescriptor Insert([NotNull] this IList descriptors, + int index, + [NotNull] Type inputFormatterType) + { + if (index < 0 || index > descriptors.Count) + { + throw new ArgumentOutOfRangeException("index"); + } + + var descriptor = new InputFormatterDescriptor(inputFormatterType); + descriptors.Insert(index, descriptor); + return descriptor; + } + + /// + /// Adds an to a descriptor collection. + /// + /// A list of InputFormatterDescriptors + /// An instance. + /// InputFormatterDescriptor representing the added instance. + public static InputFormatterDescriptor Add([NotNull] this IList descriptors, + [NotNull] IInputFormatter inputFormatter) + { + var descriptor = new InputFormatterDescriptor(inputFormatter); + descriptors.Add(descriptor); + return descriptor; + } + + /// + /// Insert an to a descriptor collection. + /// + /// A list of InputFormatterDescriptors + /// An instance. + /// InputFormatterDescriptor representing the added instance. + public static InputFormatterDescriptor Insert([NotNull] this IList descriptors, + int index, + [NotNull] IInputFormatter inputFormatter) + { + if (index < 0 || index > descriptors.Count) + { + throw new ArgumentOutOfRangeException("index"); + } + + var descriptor = new InputFormatterDescriptor(inputFormatter); + descriptors.Insert(index, descriptor); + return descriptor; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeWithQualityHeaderValue.cs b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeWithQualityHeaderValue.cs index 49a6f2fc67..e6632d89f1 100644 --- a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeWithQualityHeaderValue.cs +++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeWithQualityHeaderValue.cs @@ -12,8 +12,8 @@ namespace Microsoft.AspNet.Mvc.HeaderValueAbstractions public static new MediaTypeWithQualityHeaderValue Parse(string input) { - var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(input); - if (mediaTypeHeaderValue == null) + MediaTypeHeaderValue mediaTypeHeaderValue; + if (!MediaTypeHeaderValue.TryParse(input, out mediaTypeHeaderValue)) { return null; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs index 9edcb396e6..d8da31d762 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/FormattingUtilities.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text; using System.Xml; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -34,35 +35,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding 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/IInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/IInputFormatter.cs index 781bbbcc26..8424e4fbe6 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/IInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/IInputFormatter.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -12,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Gets the mutable collection of media types supported by this instance. /// - IList SupportedMediaTypes { get; } + IList SupportedMediaTypes { get; } /// /// Gets the mutable collection of character encodings supported by this diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/JsonInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/JsonInputFormatter.cs index 34a9d1e862..9bcaaab953 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/JsonInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/JsonInputFormatter.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; using Newtonsoft.Json; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -15,23 +16,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public class JsonInputFormatter : IInputFormatter { private const int DefaultMaxDepth = 32; - private readonly List _supportedEncodings; - private readonly List _supportedMediaTypes; private JsonSerializerSettings _jsonSerializerSettings; public JsonInputFormatter() { - _supportedMediaTypes = new List - { - "application/json", - "text/json" - }; - - _supportedEncodings = new List - { - new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), - new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true) - }; + SupportedEncodings = new List(); + SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + SupportedMediaTypes = new List(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json")); _jsonSerializerSettings = new JsonSerializerSettings { @@ -48,16 +42,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - public IList SupportedMediaTypes - { - get { return _supportedMediaTypes; } - } + public IList SupportedMediaTypes { get; private set; } /// - public IList SupportedEncodings - { - get { return _supportedEncodings; } - } + public IList SupportedEncodings { get; private set; } /// /// Gets or sets the used to configure the . @@ -94,9 +82,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return; } + MediaTypeHeaderValue requestContentType = null; + MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType); + // Get the character encoding for the content // Never non-null since SelectCharacterEncoding() throws in error / not found scenarios - var effectiveEncoding = SelectCharacterEncoding(request.GetContentType()); + var effectiveEncoding = SelectCharacterEncoding(requestContentType); context.Model = await ReadInternal(context, effectiveEncoding); } @@ -124,12 +115,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return JsonSerializer.Create(SerializerSettings); } - private bool IsSupportedContentType(ContentTypeHeaderValue contentType) - { - return contentType != null && - _supportedMediaTypes.Contains(contentType.ContentType, StringComparer.OrdinalIgnoreCase); - } - private Task ReadInternal(InputFormatterContext context, Encoding effectiveEncoding) { @@ -172,17 +157,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } - private Encoding SelectCharacterEncoding(ContentTypeHeaderValue contentType) + private Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType) { if (contentType != null) { // Find encoding based on content type charset parameter - var charset = contentType.CharSet; - if (!string.IsNullOrWhiteSpace(contentType.CharSet)) + var charset = contentType.Charset; + if (!string.IsNullOrWhiteSpace(contentType.Charset)) { - for (var i = 0; i < _supportedEncodings.Count; i++) + foreach (var supportedEncoding in SupportedEncodings) { - var supportedEncoding = _supportedEncodings[i]; if (string.Equals(charset, supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) { return supportedEncoding; @@ -191,9 +175,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } - if (_supportedEncodings.Count > 0) + if (SupportedEncodings.Count > 0) { - return _supportedEncodings[0]; + return SupportedEncodings[0]; } // No supported encoding was found so there is no way for us to start reading. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/TempInputFormatterProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/TempInputFormatterProvider.cs deleted file mode 100644 index 0709fed323..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/TempInputFormatterProvider.cs +++ /dev/null @@ -1,52 +0,0 @@ -// 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.Globalization; -using System.Linq; -using Microsoft.Framework.DependencyInjection; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class TempInputFormatterProvider : IInputFormatterProvider - { - private IInputFormatter[] _formatters; - - public IInputFormatter GetInputFormatter(InputFormatterProviderContext context) - { - var request = context.HttpContext.Request; - - var formatters = _formatters; - - if (formatters == null) - { - formatters = context.HttpContext.RequestServices.GetService>() - .ToArray(); - - _formatters = formatters; - } - - var contentType = request.GetContentType(); - if (contentType == null) - { - // TODO: http exception? - throw new InvalidOperationException("400: Bad Request"); - } - - for (var i = 0; i < formatters.Length; i++) - { - var formatter = formatters[i]; - if (formatter.SupportedMediaTypes.Contains(contentType.ContentType, StringComparer.OrdinalIgnoreCase)) - { - return formatter; - } - } - - // TODO: Http exception - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, - "415: Unsupported content type {0}", - contentType)); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs index a8a06c197f..cf7751095b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlDataContractSerializerInputFormatter.cs @@ -9,6 +9,7 @@ using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using System.Xml; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -18,8 +19,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public class XmlDataContractSerializerInputFormatter : IInputFormatter { - private readonly IList _supportedEncodings; - private readonly IList _supportedMediaTypes; private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); /// @@ -27,34 +26,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public XmlDataContractSerializerInputFormatter() { - _supportedMediaTypes = new List - { - "application/xml", - "text/xml" - }; - - _supportedEncodings = new List - { - Encodings.UTF8EncodingWithoutBOM, - Encodings.UTF16EncodingLittleEndian - }; + SupportedEncodings = new List(); + SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + SupportedMediaTypes = new List(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml")); } - /// - /// Returns the list of supported encodings. - /// - public IList SupportedEncodings - { - get { return _supportedEncodings; } - } + /// + public IList SupportedMediaTypes { get; private set; } - /// - /// Returns the list of supported Media Types. - /// - public IList SupportedMediaTypes - { - get { return _supportedMediaTypes; } - } + /// + public IList SupportedEncodings { get; private set; } /// /// Indicates the acceptable input XML depth. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs index bbeaac500d..3013480a4d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Formatters/XmlSerializerInputFormatter.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -18,8 +19,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public class XmlSerializerInputFormatter : IInputFormatter { - private readonly IList _supportedEncodings; - private readonly IList _supportedMediaTypes; private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); /// @@ -27,34 +26,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public XmlSerializerInputFormatter() { - _supportedMediaTypes = new List - { - "application/xml", - "text/xml" - }; - - _supportedEncodings = new List - { - Encodings.UTF8EncodingWithoutBOM, - Encodings.UTF16EncodingLittleEndian - }; + SupportedEncodings = new List(); + SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + SupportedMediaTypes = new List(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml")); } - /// - /// Returns the list of supported encodings. - /// - public IList SupportedEncodings - { - get { return _supportedEncodings; } - } + /// + public IList SupportedMediaTypes { get; private set; } - /// - /// Returns the list of supported Media Types. - /// - public IList SupportedMediaTypes - { - get { return _supportedMediaTypes; } - } + /// + public IList SupportedEncodings { get; private set; } /// /// Indicates the acceptable input XML depth. @@ -105,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Called during deserialization to get the . /// - /// The used during serialization and deserialization. + /// The used during deserialization. protected virtual XmlSerializer CreateXmlSerializer(Type type) { return new XmlSerializer(type); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ContentTypeHeaderValue.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ContentTypeHeaderValue.cs deleted file mode 100644 index 6989da758d..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ContentTypeHeaderValue.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 Microsoft.AspNet.Mvc.ModelBinding -{ - internal class ContentTypeHeaderValue - { - public ContentTypeHeaderValue([NotNull] string contentType, - string charSet) - { - ContentType = contentType; - CharSet = charSet; - } - - public string ContentType { get; private set; } - - public string CharSet { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/HttpRequestExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/HttpRequestExtensions.cs deleted file mode 100644 index de45dc7dca..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/HttpRequestExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - internal static class HttpRequestExtensions - { - private const string ContentTypeHeader = "Content-Type"; - private const string CharSetToken = "charset="; - - public static ContentTypeHeaderValue GetContentType(this HttpRequest httpRequest) - { - var headerValue = httpRequest.Headers[ContentTypeHeader]; - if (!string.IsNullOrEmpty(headerValue)) - { - var tokens = headerValue.Split(new[] { ';' }, 2); - string charSet = null; - if (tokens.Length > 1 && - tokens[1].TrimStart().StartsWith(CharSetToken, StringComparison.OrdinalIgnoreCase)) - { - charSet = tokens[1].TrimStart().Substring(CharSetToken.Length); - } - return new ContentTypeHeaderValue(tokens[0], charSet); - } - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs index cbd1406b79..a276309c27 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs @@ -4,12 +4,14 @@ using System; using System.Globalization; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; namespace Microsoft.AspNet.Mvc.ModelBinding { public class FormValueProviderFactory : IValueProviderFactory { - private const string FormEncodedContentType = "application/x-www-form-urlencoded"; + private static MediaTypeHeaderValue _formEncodedContentType = + MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded"); public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context) { @@ -26,9 +28,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool IsSupportedContentType(HttpRequest request) { - var contentType = request.GetContentType(); - return contentType != null && - string.Equals(contentType.ContentType, FormEncodedContentType, StringComparison.OrdinalIgnoreCase); + MediaTypeHeaderValue requestContentType = null; + return MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType) && + _formEncodedContentType.IsSubsetOf(requestContentType); } private static CultureInfo GetCultureInfo(HttpRequest request) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index a3c5f19c7b..76fafa0c2b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -8,6 +8,7 @@ "Microsoft.AspNet.Mvc.Common": "", "Microsoft.DataAnnotations": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Microsoft.AspNet.Mvc.HeaderValueAbstractions": "1.0.0-*", "Newtonsoft.Json": "5.0.8" }, "frameworks": { diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index f6c7f634e4..7d42cb0d39 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -1,7 +1,9 @@ // 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.Collections.Generic; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.OptionDescriptors; using Microsoft.AspNet.Mvc.Razor; using Microsoft.Framework.OptionsModel; @@ -43,6 +45,11 @@ namespace Microsoft.AspNet.Mvc options.OutputFormatters.Add( new XmlSerializerOutputFormatter(XmlOutputFormatter.GetDefaultXmlWriterSettings())); + // Set up default input formatters. + options.InputFormatters.Add(new JsonInputFormatter()); + options.InputFormatters.Add(new XmlSerializerInputFormatter()); + options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter()); + // Set up ValueProviders options.ValueProviderFactories.Add(new RouteValueValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 3fe7dfae89..e81c316a82 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -64,10 +64,8 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Scoped(); - yield return describe.Transient(); - yield return describe.Transient(); - yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Transient(); yield return describe.Transient(); yield return describe.Scoped(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorExtensionTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorExtensionTest.cs new file mode 100644 index 0000000000..a6ccaf18c4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorExtensionTest.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.OptionDescriptors; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class InputFormatterDescriptorExtensionTest + { + [Theory] + [InlineData(-1)] + [InlineData(5)] + public void Insert_WithType_ThrowsIfIndexIsOutOfBounds(int index) + { + // Arrange + var collection = new List + { + new InputFormatterDescriptor(Mock.Of()), + new InputFormatterDescriptor(Mock.Of()) + }; + + // Act & Assert + Assert.Throws("index", + () => collection.Insert(index, typeof(IInputFormatter))); + } + + [Theory] + [InlineData(-2)] + [InlineData(3)] + public void Insert_WithInstance_ThrowsIfIndexIsOutOfBounds(int index) + { + // Arrange + var collection = new List + { + new InputFormatterDescriptor(Mock.Of()), + new InputFormatterDescriptor(Mock.Of()) + }; + var formatter = Mock.Of(); + + // Act & Assert + Assert.Throws("index", () => collection.Insert(index, formatter)); + } + + [InlineData] + public void InputFormatterDescriptors_AddsTypesAndInstances() + { + // Arrange + var formatter1 = Mock.Of(); + var formatter2 = Mock.Of(); + var type1 = typeof(JsonInputFormatter); + var type2 = typeof(IInputFormatter); + var collection = new List(); + + // Act + collection.Add(formatter1); + collection.Insert(1, formatter2); + collection.Add(type1); + collection.Insert(2, type2); + + // Assert + Assert.Equal(4, collection.Count); + Assert.Equal(formatter1, collection[0].Instance); + Assert.Equal(formatter2, collection[1].Instance); + Assert.Equal(type2, collection[2].OptionType); + Assert.Equal(type1, collection[3].OptionType); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorTest.cs new file mode 100644 index 0000000000..027a75d1f8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/InputFormatterDescriptorTest.cs @@ -0,0 +1,25 @@ +// 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.OptionDescriptors; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class InputFormatterDescriptorTest + { + [Fact] + public void ConstructorThrows_IfTypeIsNotInputFormatter() + { + // Arrange + var expected = "The type 'System.String' must derive from " + + "'Microsoft.AspNet.Mvc.ModelBinding.IInputFormatter'."; + + var type = typeof(string); + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => new InputFormatterDescriptor(type), "type", expected); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs index 0dae828d84..6438b8ef91 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs @@ -37,6 +37,23 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(sampleInputInt.ToString(), await response.ReadBodyAsStringAsync()); } + [Fact] + public async Task JsonInputFormatter_IsSelectedForJsonRequest() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.Handler; + var sampleInputInt = 10; + var input = "{\"SampleInt\":10}"; + + // Act + var response = await client.PostAsync("http://localhost/Home/Index", input, "application/json"); + + //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. } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/JsonInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/JsonInputFormatterTest.cs index b0106c3d9b..be4a915022 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/JsonInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/JsonInputFormatterTest.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var mediaType = formatter.SupportedMediaTypes[0]; // Assert - Assert.Equal("application/json", mediaType); + Assert.Equal("application/json", mediaType.RawValue); } public static IEnumerable JsonFormatterReadSimpleTypesData diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs index cd8494ae14..110e69d33e 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlDataContractSerializerInputFormatterTests.cs @@ -50,8 +50,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var formatter = new XmlDataContractSerializerInputFormatter(); // Assert - Assert.True(formatter.SupportedMediaTypes.Contains("application/xml")); - Assert.True(formatter.SupportedMediaTypes.Contains("text/xml")); + Assert.True(formatter.SupportedMediaTypes + .Select(content => content.RawValue) + .Contains("application/xml")); + Assert.True(formatter.SupportedMediaTypes + .Select(content => content.RawValue) + .Contains("text/xml")); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs index 2b21d1e20f..4bc8b0a6e6 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Formatters/XmlSerializerInputFormatterTests.cs @@ -41,8 +41,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var formatter = new XmlSerializerInputFormatter(); // Assert - Assert.True(formatter.SupportedMediaTypes.Contains("application/xml")); - Assert.True(formatter.SupportedMediaTypes.Contains("text/xml")); + Assert.True(formatter.SupportedMediaTypes + .Select(content => content.RawValue) + .Contains("application/xml")); + Assert.True(formatter.SupportedMediaTypes + .Select(content => content.RawValue) + .Contains("text/xml")); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs index b08e59869d..ff7d61dd23 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs @@ -52,10 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var collection = Mock.Of(); var request = new Mock(); request.Setup(f => f.GetFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection)); - - var mockHeader = new Mock(); - mockHeader.Setup(h => h["Content-Type"]).Returns(contentType); - request.SetupGet(r => r.Headers).Returns(mockHeader.Object); + request.SetupGet(r => r.ContentType).Returns(contentType); var context = new Mock(); context.SetupGet(c => c.Request).Returns(request.Object); diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index 57daadadcf..ab6027e51d 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -80,5 +80,22 @@ namespace Microsoft.AspNet.Mvc Assert.IsType(mvcOptions.OutputFormatters[3].Instance); Assert.IsType(mvcOptions.OutputFormatters[4].Instance); } + + [Fact] + public void Setup_SetsUpInputFormatters() + { + // Arrange + var mvcOptions = new MvcOptions(); + var setup = new MvcOptionsSetup(); + + // Act + setup.Setup(mvcOptions); + + // Assert + Assert.Equal(3, mvcOptions.InputFormatters.Count); + Assert.IsType(mvcOptions.InputFormatters[0].Instance); + Assert.IsType(mvcOptions.InputFormatters[1].Instance); + Assert.IsType(mvcOptions.InputFormatters[2].Instance); + } } } \ No newline at end of file