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
This commit is contained in:
harshgMSFT 2014-07-25 16:04:19 -07:00
parent 5708f7592b
commit 19f3f78b3e
12 changed files with 238 additions and 19 deletions

View File

@ -16,7 +16,8 @@ namespace Microsoft.AspNet.Mvc
/// <summary>
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
/// </summary>
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);
}
}

View File

@ -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;
}

View File

@ -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
{
/// <summary>
/// Always writes a string value to the response, regardless of requested content type.
/// </summary>
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);
}
}
}
}

View File

@ -30,6 +30,7 @@
<Compile Include="ActionDescriptorProviderContext.cs" />
<Compile Include="ActionDescriptorsCollection.cs" />
<Compile Include="ActionResults\HttpNotFoundResult.cs" />
<Compile Include="Formatters\TextPlainFormatter.cs" />
<Compile Include="OptionDescriptors\DefaultModelBinderProvider.cs" />
<Compile Include="OptionDescriptors\DefaultValueProviderFactoryProvider.cs" />
<Compile Include="OptionDescriptors\DefaultViewEngineProvider.cs" />

View File

@ -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()

View File

@ -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

View File

@ -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<MediaTypeHeaderValue>();
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<HttpResponse>();
var tempContentType = string.Empty;
httpResponse.SetupProperty<string>(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<IOutputFormatter>()
{ new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false) };
{
new TextPlainFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)
};
}
}
}

View File

@ -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<HttpContext>();
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);
}
}
}
}

View File

@ -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<object[]> 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<HttpContext>();
mockHttpContext.SetupGet(o => o.Response.Body)
.Returns(tempMemoryStream);
// Act
await formatter.WriteResponseBodyAsync(formatterContext);
// Assert
Assert.Equal(0, tempMemoryStream.Length);
}
}
}

View File

@ -32,6 +32,7 @@
<Compile Include="ActionResults\RedirectResultTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsActionSelectionTests.cs" />
<Compile Include="AntiXsrf\AntiForgeryOptionsTests.cs" />
<Compile Include="Formatters\TextPlainFormatterTests.cs" />
<Compile Include="OptionDescriptors\DefaultModelBindersProviderTest.cs" />
<Compile Include="OptionDescriptors\DefaultValueProviderFactoryProviderTest.cs" />
<Compile Include="OptionDescriptors\DefaultViewEngineProviderTest.cs" />
@ -46,7 +47,7 @@
<Compile Include="ExpiringFileInfoCacheTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsTests.cs" />
<Compile Include="Formatters\OutputFormatterTests.cs" />
<Compile Include="MediaTypeWithQualityHeaderValueTests.cs" />
<Compile Include="MediaTypeWithQualityHeaderValueTests.cs" />
<Compile Include="ParameterBinding\ModelBindingHelperTest.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />

View File

@ -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

View File

@ -72,8 +72,9 @@ namespace Microsoft.AspNet.Mvc
setup.Setup(mvcOptions);
// Assert
Assert.Equal(1, mvcOptions.OutputFormatters.Count);
Assert.IsType<JsonOutputFormatter>(mvcOptions.OutputFormatters[0].Instance);
Assert.Equal(2, mvcOptions.OutputFormatters.Count);
Assert.IsType<TextPlainFormatter>(mvcOptions.OutputFormatters[0].Instance);
Assert.IsType<JsonOutputFormatter>(mvcOptions.OutputFormatters[1].Instance);
}
}
}