From 13e1c9d5e1509b0df5c7a2317b6380fa1a401784 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 6 Feb 2015 12:16:15 -0800 Subject: [PATCH] added InputFormatter base type --- .../Formatters/InputFormatter.cs | 105 ++++++++++++++++ .../Formatters/JsonInputFormatter.cs | 114 ++++-------------- .../Formatters/OutputFormatter.cs | 2 +- ...XmlDataContractSerializerInputFormatter.cs | 88 ++++---------- .../XmlSerializerInputFormatter.cs | 88 ++++---------- .../InputFormatterTests.cs | 37 ++++++ .../Controllers/InputFormatterController.cs | 11 ++ test/WebSites/FormatterWebSite/Startup.cs | 1 + .../FormatterWebSite/StringInputFormatter.cs | 34 ++++++ 9 files changed, 254 insertions(+), 226 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs create mode 100644 test/WebSites/FormatterWebSite/StringInputFormatter.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs new file mode 100644 index 0000000000..9962de7a39 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs @@ -0,0 +1,105 @@ +// 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.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Reads an object from the request body. + /// + public abstract class InputFormatter : IInputFormatter + { + /// + /// Gets the mutable collection of character encodings supported by + /// this . The encodings are + /// used when reading the data. + /// + public IList SupportedEncodings { get; } = new List(); + + /// + /// Gets the mutable collection of elements supported by + /// this . + /// + public IList SupportedMediaTypes { get; } = new List(); + + protected object GetDefaultValueForType(Type modelType) + { + if (modelType.GetTypeInfo().IsValueType) + { + return Activator.CreateInstance(modelType); + } + + return null; + } + + /// + public virtual bool CanRead(InputFormatterContext context) + { + var contentType = context.ActionContext.HttpContext.Request.ContentType; + MediaTypeHeaderValue requestContentType; + if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType)) + { + return false; + } + + return SupportedMediaTypes + .Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType)); + } + + /// + public virtual async Task ReadAsync(InputFormatterContext context) + { + var request = context.ActionContext.HttpContext.Request; + if (request.ContentLength == 0) + { + return GetDefaultValueForType(context.ModelType); + } + + return await ReadRequestBodyAsync(context); + } + + /// + /// Reads the request body. + /// + /// The associated with the call. + /// A task which can read the request body. + public abstract Task ReadRequestBodyAsync(InputFormatterContext context); + + /// + /// Returns encoding based on content type charset parameter. + /// + protected Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType) + { + if (contentType != null) + { + var charset = contentType.Charset; + if (!string.IsNullOrWhiteSpace(contentType.Charset)) + { + foreach (var supportedEncoding in SupportedEncodings) + { + 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.FormatInputFormatterNoEncoding(GetType().FullName)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonInputFormatter.cs index 824947263f..79f6cefa63 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonInputFormatter.cs @@ -2,29 +2,24 @@ // 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.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.Core; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; namespace Microsoft.AspNet.Mvc { - public class JsonInputFormatter : IInputFormatter + public class JsonInputFormatter : InputFormatter { private const int DefaultMaxDepth = 32; private JsonSerializerSettings _jsonSerializerSettings; public JsonInputFormatter() { - 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")); @@ -42,12 +37,6 @@ public JsonInputFormatter() }; } - /// - public IList SupportedMediaTypes { get; private set; } - - /// - public IList SupportedEncodings { get; private set; } - /// /// Gets or sets the used to configure the . /// @@ -72,31 +61,10 @@ public JsonSerializerSettings SerializerSettings public bool CaptureDeserilizationErrors { get; set; } /// - public bool CanRead(InputFormatterContext context) - { - var contentType = context.ActionContext.HttpContext.Request.ContentType; - MediaTypeHeaderValue requestContentType; - if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType)) - { - return false; - } - - return SupportedMediaTypes - .Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType)); - } - - /// - public async Task ReadAsync([NotNull] InputFormatterContext context) + public override Task ReadRequestBodyAsync([NotNull] InputFormatterContext context) { + var type = context.ModelType; var request = context.ActionContext.HttpContext.Request; - if (request.ContentLength == 0) - { - var modelType = context.ModelType; - var model = modelType.GetTypeInfo().IsValueType ? Activator.CreateInstance(modelType) : - null; - return model; - } - MediaTypeHeaderValue requestContentType = null; MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType); @@ -104,38 +72,6 @@ public async Task ReadAsync([NotNull] InputFormatterContext context) // Never non-null since SelectCharacterEncoding() throws in error / not found scenarios var effectiveEncoding = SelectCharacterEncoding(requestContentType); - return await ReadInternal(context, effectiveEncoding); - } - - /// - /// Called during deserialization to get the . - /// - /// The for the read. - /// The from which to read. - /// The to use when reading. - /// The used during deserialization. - public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context, - [NotNull] Stream readStream, - [NotNull] Encoding effectiveEncoding) - { - return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); - } - - /// - /// Called during deserialization to get the . - /// - /// The used during serialization and deserialization. - public virtual JsonSerializer CreateJsonSerializer() - { - return JsonSerializer.Create(SerializerSettings); - } - - private Task ReadInternal(InputFormatterContext context, - Encoding effectiveEncoding) - { - var type = context.ModelType; - var request = context.ActionContext.HttpContext.Request; - using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding)) { jsonReader.CloseInput = false; @@ -172,31 +108,27 @@ private Task ReadInternal(InputFormatterContext context, } } - private Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType) + /// + /// Called during deserialization to get the . + /// + /// The for the read. + /// The from which to read. + /// The to use when reading. + /// The used during deserialization. + public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context, + [NotNull] Stream readStream, + [NotNull] Encoding effectiveEncoding) { - if (contentType != null) - { - // Find encoding based on content type charset parameter - var charset = contentType.Charset; - if (!string.IsNullOrWhiteSpace(contentType.Charset)) - { - foreach (var supportedEncoding in SupportedEncodings) - { - if (string.Equals(charset, supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) - { - return supportedEncoding; - } - } - } - } - - if (SupportedEncodings.Count > 0) - { - return SupportedEncodings[0]; - } + return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); + } - // No supported encoding was found so there is no way for us to start reading. - throw new InvalidOperationException(Resources.FormatInputFormatterNoEncoding(GetType().FullName)); + /// + /// Called during deserialization to get the . + /// + /// The used during serialization and deserialization. + public virtual JsonSerializer CreateJsonSerializer() + { + return JsonSerializer.Create(SerializerSettings); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs index 81ef066f9d..247c58a03f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -34,7 +34,7 @@ protected OutputFormatter() /// this . The encodings are /// used when writing the data. /// - public IList SupportedEncodings { get; private set; } + public IList SupportedEncodings { get; } /// /// Gets the mutable collection of elements supported by diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs index 8b0aad030c..810fdabec5 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs @@ -4,10 +4,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Reflection; using System.Runtime.Serialization; -using System.Text; using System.Threading.Tasks; using System.Xml; using Microsoft.AspNet.Mvc.Internal; @@ -19,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Xml /// This class handles deserialization of input XML data /// to strongly-typed objects using . /// - public class XmlDataContractSerializerInputFormatter : IInputFormatter + public class XmlDataContractSerializerInputFormatter : InputFormatter { private DataContractSerializerSettings _serializerSettings; private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); @@ -29,11 +26,9 @@ public class XmlDataContractSerializerInputFormatter : IInputFormatter /// public XmlDataContractSerializerInputFormatter() { - 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")); @@ -49,12 +44,6 @@ public XmlDataContractSerializerInputFormatter() /// public IList WrapperProviderFactories { get; } - /// - public IList SupportedMediaTypes { get; } - - /// - public IList SupportedEncodings { get; } - /// /// Indicates the acceptable input XML depth. /// @@ -73,20 +62,6 @@ public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas get { return _readerQuotas; } } - /// - public bool CanRead(InputFormatterContext context) - { - var contentType = context.ActionContext.HttpContext.Request.ContentType; - MediaTypeHeaderValue requestContentType; - if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType)) - { - return false; - } - - return SupportedMediaTypes - .Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType)); - } - /// /// Gets or sets the used to configure the /// . @@ -110,15 +85,30 @@ public DataContractSerializerSettings SerializerSettings /// /// The input formatter context which contains the body to be read. /// Task which reads the input. - public async Task ReadAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync(InputFormatterContext context) { var request = context.ActionContext.HttpContext.Request; - if (request.ContentLength == 0) + + using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body))) { - return GetDefaultValueForType(context.ModelType); - } + var type = GetSerializableType(context.ModelType); + + var serializer = CreateSerializer(type); + + var deserializedObject = serializer.ReadObject(xmlReader); - return await ReadInternalAsync(context); + // Unwrap only if the original type was wrapped. + if (type != context.ModelType) + { + var unwrappable = deserializedObject as IUnwrappable; + if (unwrappable != null) + { + deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); + } + } + + return Task.FromResult(deserializedObject); + } } /// @@ -154,41 +144,5 @@ protected virtual DataContractSerializer CreateSerializer([NotNull] Type type) { return new DataContractSerializer(type, _serializerSettings); } - - private object GetDefaultValueForType(Type modelType) - { - if (modelType.GetTypeInfo().IsValueType) - { - return Activator.CreateInstance(modelType); - } - - return null; - } - - private Task ReadInternalAsync(InputFormatterContext context) - { - var request = context.ActionContext.HttpContext.Request; - - using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body))) - { - var type = GetSerializableType(context.ModelType); - - var serializer = CreateSerializer(type); - - var deserializedObject = serializer.ReadObject(xmlReader); - - // Unwrap only if the original type was wrapped. - if (type != context.ModelType) - { - var unwrappable = deserializedObject as IUnwrappable; - if (unwrappable != null) - { - deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); - } - } - - return Task.FromResult(deserializedObject); - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs index c187a0412c..bc661b4f91 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs @@ -4,9 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; @@ -19,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Xml /// This class handles deserialization of input XML data /// to strongly-typed objects using /// - public class XmlSerializerInputFormatter : IInputFormatter + public class XmlSerializerInputFormatter : InputFormatter { private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); @@ -28,11 +25,9 @@ public class XmlSerializerInputFormatter : IInputFormatter /// public XmlSerializerInputFormatter() { - 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")); @@ -46,12 +41,6 @@ public XmlSerializerInputFormatter() /// public IList WrapperProviderFactories { get; } - /// - public IList SupportedMediaTypes { get; } - - /// - public IList SupportedEncodings { get; } - /// /// Indicates the acceptable input XML depth. /// @@ -70,34 +59,35 @@ public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas get { return _readerQuotas; } } - /// - public bool CanRead(InputFormatterContext context) - { - var contentType = context.ActionContext.HttpContext.Request.ContentType; - MediaTypeHeaderValue requestContentType; - if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType)) - { - return false; - } - - return SupportedMediaTypes - .Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType)); - } - /// /// 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) + public override Task ReadRequestBodyAsync(InputFormatterContext context) { var request = context.ActionContext.HttpContext.Request; - if (request.ContentLength == 0) + + using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body))) { - return GetDefaultValueForType(context.ModelType); - } + var type = GetSerializableType(context.ModelType); + + var serializer = CreateSerializer(type); + + var deserializedObject = serializer.Deserialize(xmlReader); - return await ReadInternalAsync(context); + // Unwrap only if the original type was wrapped. + if (type != context.ModelType) + { + var unwrappable = deserializedObject as IUnwrappable; + if (unwrappable != null) + { + deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); + } + } + + return Task.FromResult(deserializedObject); + } } /// @@ -132,41 +122,5 @@ protected virtual XmlSerializer CreateSerializer(Type type) { return new XmlSerializer(type); } - - private object GetDefaultValueForType(Type modelType) - { - if (modelType.GetTypeInfo().IsValueType) - { - return Activator.CreateInstance(modelType); - } - - return null; - } - - private Task ReadInternalAsync(InputFormatterContext context) - { - var request = context.ActionContext.HttpContext.Request; - - using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body))) - { - var type = GetSerializableType(context.ModelType); - - var serializer = CreateSerializer(type); - - var deserializedObject = serializer.Deserialize(xmlReader); - - // Unwrap only if the original type was wrapped. - if (type != context.ModelType) - { - var unwrappable = deserializedObject as IUnwrappable; - if (unwrappable != null) - { - deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); - } - } - - return Task.FromResult(deserializedObject); - } - } } } \ 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 81bec2cac2..95060df7d2 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs @@ -163,5 +163,42 @@ public async Task JsonInputFormatter_IsModelStateValid_ForTransferEncodingChunk( Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedSampleIntValue.ToString(), responseBody); } + + [Theory] + [InlineData("utf-8")] + [InlineData("unicode")] + public async Task CustomFormatter_IsSelected_ForSupportedContentTypeAndEncoding(string encoding) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var content = new StringContent("Test Content", Encoding.GetEncoding(encoding), "text/plain"); + + // Act + var response = await client.PostAsync("http://localhost/InputFormatter/ReturnInput/", content); + var responseBody = await response.Content.ReadAsStringAsync(); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test Content", responseBody); + } + + [Theory] + [InlineData("image/png")] + [InlineData("image/jpeg")] + public async Task CustomFormatter_NotSelected_ForUnsupportedContentType(string contentType) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var content = new StringContent("Test Content", Encoding.UTF8, contentType); + + // Act + var response = await client.PostAsync("http://localhost/InputFormatter/ReturnInput/", content); + var responseBody = await response.Content.ReadAsStringAsync(); + + //Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } } } \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs b/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs index e84973e242..a09ac4553a 100644 --- a/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs +++ b/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.WebUtilities; namespace FormatterWebSite.Controllers { @@ -35,5 +36,15 @@ public object ActionFilterHandlesError([FromBody] DummyClass dummy) { return dummy; } + + public IActionResult ReturnInput([FromBody] string test) + { + if (!ModelState.IsValid) + { + return new HttpStatusCodeResult(StatusCodes.Status400BadRequest); + } + + return Content(test); + } } } \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs index d26a72dab9..7cfcb56a9c 100644 --- a/test/WebSites/FormatterWebSite/Startup.cs +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -25,6 +25,7 @@ public void Configure(IApplicationBuilder app) options.ValidationExcludeFilters.Add(typeof(Supplier)); options.AddXmlDataContractSerializerFormatter(); + options.InputFormatters.Add(new StringInputFormatter()); }); }); diff --git a/test/WebSites/FormatterWebSite/StringInputFormatter.cs b/test/WebSites/FormatterWebSite/StringInputFormatter.cs new file mode 100644 index 0000000000..d6ea797b17 --- /dev/null +++ b/test/WebSites/FormatterWebSite/StringInputFormatter.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.Net.Http.Headers; + +namespace FormatterWebSite +{ + public class StringInputFormatter : InputFormatter + { + public StringInputFormatter() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain")); + + SupportedEncodings.Add(Encoding.UTF8); + SupportedEncodings.Add(Encoding.Unicode); + } + + public override Task ReadRequestBodyAsync(InputFormatterContext context) + { + var request = context.ActionContext.HttpContext.Request; + MediaTypeHeaderValue requestContentType = null; + MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType); + var effectiveEncoding = SelectCharacterEncoding(requestContentType); + + using (var reader = new StreamReader(request.Body, effectiveEncoding)) + { + var stringContent = reader.ReadToEnd(); + return Task.FromResult(stringContent); + } + } + } +} \ No newline at end of file