From 19f3f78b3e7132fca2a1e7092e49389a3a623151 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Fri, 25 Jul 2014 16:04:19 -0700 Subject: [PATCH] Adding TextPlainFormatter to always handle returning strings as text\plain format. Conflicts: src/Microsoft.AspNet.Mvc.Common/Encodings.cs src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs --- src/Microsoft.AspNet.Mvc.Common/Encodings.cs | 5 +- .../Formatters/OutputFormatter.cs | 2 + .../Formatters/TextPlainFormatter.cs | 60 +++++++++++++++ .../Microsoft.AspNet.Mvc.Core.kproj | 1 + .../ReflectedActionInvoker.cs | 5 +- src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 3 +- .../ActionResults/ObjectContentResultTests.cs | 45 +++++++++-- .../Formatters/OutputFormatterTests.cs | 47 +++++++++++- .../Formatters/TextPlainFormatterTests.cs | 75 +++++++++++++++++++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 3 +- .../ValueProviderTests.cs | 6 +- .../MvcOptionSetupTest.cs | 5 +- 12 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/TextPlainFormatter.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Formatters/TextPlainFormatterTests.cs diff --git a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs index 5ab65d219a..5c647d80dd 100644 --- a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs +++ b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs @@ -16,7 +16,8 @@ namespace Microsoft.AspNet.Mvc /// /// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes. /// - public static readonly Encoding UTF16EncodingLittleEndian - = 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/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs index f74f46f495..9bd41f033d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -127,6 +127,8 @@ namespace Microsoft.AspNet.Mvc // Override the content type value even if one already existed. selectedMediaType.Charset = selectedEncoding.WebName; + + context.SelectedContentType = context.SelectedContentType ?? selectedMediaType; var response = context.ActionContext.HttpContext.Response; response.ContentType = selectedMediaType.RawValue; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/TextPlainFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/TextPlainFormatter.cs new file mode 100644 index 0000000000..d6e51c8030 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/TextPlainFormatter.cs @@ -0,0 +1,60 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Always writes a string value to the response, regardless of requested content type. + /// + public class TextPlainFormatter : OutputFormatter + { + public TextPlainFormatter() + { + SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain")); + } + + public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) + { + // Ignore the passed in content type, if the object is string + // always return it as a text/plain format. + if(context.DeclaredType == typeof(string)) + { + return true; + } + + if (context.Object is string) + { + return true; + } + + return false; + } + + public override async Task WriteResponseBodyAsync(OutputFormatterContext context) + { + var valueAsString = (string)context.Object; + if (valueAsString == null) + { + // if the value is null don't write anything. + return; + } + + var response = context.ActionContext.HttpContext.Response; + using (var writer = new StreamWriter(response.Body, context.SelectedEncoding, 1024, leaveOpen: true)) + { + await writer.WriteAsync(valueAsString); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 209cd370da..7c307edad4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -30,6 +30,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs index 17996810eb..77b59bb781 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs @@ -120,7 +120,10 @@ namespace Microsoft.AspNet.Mvc Resources.FormatActionResult_ActionReturnValueCannotBeNull(actualReturnType)); } - return new ObjectResult(actionReturnValue); + return new ObjectResult(actionReturnValue) + { + DeclaredType = actualReturnType + }; } private IFilter[] GetFilters() diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 2ed21772a1..84bcfb2947 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -33,7 +33,8 @@ namespace Microsoft.AspNet.Mvc options.ModelBinders.Add(new ComplexModelDtoModelBinder()); // Set up default output formatters. - options.OutputFormatters.Add(new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), + options.OutputFormatters.Add(new TextPlainFormatter()); + options.OutputFormatters.Add(new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)); // Set up ValueProviders diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs index f5f8087611..5be62d67c4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs @@ -109,7 +109,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults { // Arrange var expectedContentType = "application/json;charset=utf-8"; - var input = "testInput"; + + // non string value. + var input = 123; var httpResponse = GetMockHttpResponse(); var actionContext = CreateMockActionContext(httpResponse.Object); @@ -125,6 +127,30 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults httpResponse.VerifySet(r => r.ContentType = expectedContentType); } + [Fact] + public async Task ObjectResult_WithSingleContentType_TheContentTypeIsIgnoredIfTheTypeIsString() + { + // Arrange + var contentType = "application/json;charset=utf-8"; + var expectedContentType = "text/plain;charset=utf-8"; + + // string value. + var input = "1234"; + var httpResponse = GetMockHttpResponse(); + var actionContext = CreateMockActionContext(httpResponse.Object); + + // Set the content type property explicitly to a single value. + var result = new ObjectResult(input); + result.ContentTypes = new List(); + result.ContentTypes.Add(MediaTypeHeaderValue.Parse(contentType)); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + httpResponse.VerifySet(r => r.ContentType = expectedContentType); + } + [Fact] public async Task ObjectResult_MultipleContentTypes_PicksFirstFormatterWhichSupportsAnyOfTheContentTypes() { @@ -307,22 +333,21 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults httpResponse.VerifySet(r => r.StatusCode = 406); } - // TODO: Disabling since this scenario is no longer dealt with in object result. - // Re-enable once we do. - //[Fact] + [Fact] public async Task ObjectResult_Execute_CallsContentResult_SetsContent() { // Arrange - var expectedContentType = "text/plain"; + var expectedContentType = "text/plain;charset=utf-8"; var input = "testInput"; var stream = new MemoryStream(); var httpResponse = new Mock(); - var tempContentType = string.Empty; httpResponse.SetupProperty(o => o.ContentType); httpResponse.SetupGet(r => r.Body).Returns(stream); - var actionContext = CreateMockActionContext(httpResponse.Object); + var actionContext = CreateMockActionContext(httpResponse.Object, + requestAcceptHeader: null, + requestContentType: null); // Act var result = new ObjectResult(input); @@ -330,6 +355,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults // Assert httpResponse.VerifySet(r => r.ContentType = expectedContentType); + // The following verifies the correct Content was written to Body Assert.Equal(input.Length, httpResponse.Object.Body.Length); } @@ -471,7 +497,10 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults get { return new List() - { new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false) }; + { + new TextPlainFormatter(), + new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false) + }; } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs index 6e46e62ae6..085ac3ca2a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs @@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Mvc.Test } [Fact] - public void SetResponseContentHeaders_FormatterWithNoEncoding_Throws() + public void WriteResponseContentHeaders_FormatterWithNoEncoding_Throws() { // Arrange var testFormatter = new TestOutputFormatter(); @@ -96,6 +96,30 @@ namespace Microsoft.AspNet.Mvc.Test " output formatter to write content.", ex.Message); } + [Fact] + public void WriteResponseContentHeaders_NoSelectedContentType_SetsOutputFormatterContext() + { + // Arrange + var testFormatter = new DoesNotSetContext(); + var testContentType = MediaTypeHeaderValue.Parse("application/doesNotSetContext"); + var formatterContext = new OutputFormatterContext(); + var mockHttpContext = new Mock(); + mockHttpContext.SetupGet(o => o.Request.AcceptCharset) + .Returns(string.Empty); + mockHttpContext.SetupProperty(o => o.Response.ContentType); + var actionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor()); + formatterContext.ActionContext = actionContext; + + // Act + testFormatter.WriteResponseContentHeaders(formatterContext); + + // Assert + Assert.Equal(Encodings.UTF16EncodingLittleEndian.WebName, formatterContext.SelectedEncoding.WebName); + Assert.Equal(Encodings.UTF16EncodingLittleEndian, formatterContext.SelectedEncoding); + Assert.Equal("application/doesNotSetContext;charset=" + Encodings.UTF16EncodingLittleEndian.WebName, + formatterContext.SelectedContentType.RawValue); + } + private class TestOutputFormatter : OutputFormatter { public TestOutputFormatter() @@ -108,5 +132,26 @@ namespace Microsoft.AspNet.Mvc.Test return Task.FromResult(true); } } + + private class DoesNotSetContext : OutputFormatter + { + public DoesNotSetContext() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/doesNotSetContext")); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + } + + public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) + { + // Do not set the selected media Type. + // The WriteResponseContentHeader should do it for you. + return true; + } + + public override Task WriteResponseBodyAsync(OutputFormatterContext context) + { + return Task.FromResult(true); + } + } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/TextPlainFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/TextPlainFormatterTests.cs new file mode 100644 index 0000000000..12af356668 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/TextPlainFormatterTests.cs @@ -0,0 +1,75 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class TextPlainFormatterTests + { + public static IEnumerable OutputFormatterContextValues + { + get + { + // object value, bool useDeclaredTypeAsString, bool expectedCanWriteResult + yield return new object[] { "valid value", true, true }; + yield return new object[] { "valid value", false, true }; + yield return new object[] { null, true, true }; + yield return new object[] { null, false, false }; + yield return new object[] { new object(), false, false }; + } + } + + [Theory] + [MemberData("OutputFormatterContextValues")] + public void CanWriteResult_ReturnsTrueForStringTypes(object value, bool useDeclaredTypeAsString, bool expectedCanWriteResult) + { + // Arrange + var formatter = new TextPlainFormatter(); + var typeToUse = useDeclaredTypeAsString ? typeof(string) : typeof(object); + var formatterContext = new OutputFormatterContext() + { + Object = value, + DeclaredType = typeToUse + }; + + // Act + var result = formatter.CanWriteResult(formatterContext, null); + + // Assert + Assert.Equal(expectedCanWriteResult, result); + } + + [Fact] + public async Task WriteAsync_DoesNotWriteNullStrings() + { + // Arrange + var formatter = new TextPlainFormatter(); + var formatterContext = new OutputFormatterContext() + { + Object = null, + DeclaredType = typeof(string), + }; + + var tempMemoryStream = new MemoryStream(); + var mockHttpContext = new Mock(); + mockHttpContext.SetupGet(o => o.Response.Body) + .Returns(tempMemoryStream); + // Act + await formatter.WriteResponseBodyAsync(formatterContext); + + // Assert + Assert.Equal(0, tempMemoryStream.Length); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index c3c2be983e..df13c877bc 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -32,6 +32,7 @@ + @@ -46,7 +47,7 @@ - + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ValueProviderTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ValueProviderTests.cs index 9db492a810..2fae97fa1d 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ValueProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ValueProviderTests.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests _services = TestHelper.CreateServices("ValueProvidersSite"); } - [Fact(Skip = "Skipped until PR#868 is checked in.")] + [Fact] public async Task ValueProviderFactories_AreVisitedInSequentialOrder_ForValueProviders() { // Arrange @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("custom-value-provider-value", body.Trim()); } - [Fact(Skip = "Skipped until PR#868 is checked in.")] + [Fact] public async Task ValueProviderFactories_ReturnsValuesFromQueryValueProvider() { // Arrange @@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("query-value", body.Trim()); } - [Fact(Skip = "Skipped until PR#868 is checked in.")] + [Fact] public async Task ValueProviderFactories_ReturnsValuesFromRouteValueProvider() { // Arrange diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index 86f5a04781..2674329afc 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -72,8 +72,9 @@ namespace Microsoft.AspNet.Mvc setup.Setup(mvcOptions); // Assert - Assert.Equal(1, mvcOptions.OutputFormatters.Count); - Assert.IsType(mvcOptions.OutputFormatters[0].Instance); + Assert.Equal(2, mvcOptions.OutputFormatters.Count); + Assert.IsType(mvcOptions.OutputFormatters[0].Instance); + Assert.IsType(mvcOptions.OutputFormatters[1].Instance); } } } \ No newline at end of file