Adding OutputFormatter base class

This commit is contained in:
harshgMSFT 2014-07-11 11:23:14 -07:00
parent e28adbfb3d
commit 1df4738a19
16 changed files with 316 additions and 76 deletions

View File

@ -8,9 +8,16 @@ namespace Microsoft.AspNet.Mvc
internal static class Encodings
{
/// <summary>
/// Returns UTF8 Encoding without BOM and throws on invalid bytes
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
/// </summary>
public static readonly Encoding UTF8EncodingWithoutBOM
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
/// <summary>
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
/// </summary>
public static readonly Encoding UnicodeEncodingWithBOM = new UnicodeEncoding(bigEndian: false,
byteOrderMark: true,
throwOnInvalidBytes: true);
}
}

View File

@ -5,11 +5,11 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc
{
public class ObjectContentResult : ActionResult
public class ObjectResult : ActionResult
{
public object Value { get; set; }
public ObjectContentResult(object value)
public ObjectResult(object value)
{
Value = value;
}

View File

@ -0,0 +1,109 @@
// 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.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonOutputFormatter : OutputFormatter
{
private readonly JsonSerializerSettings _settings;
private readonly bool _indent;
public JsonOutputFormatter([NotNull] JsonSerializerSettings settings, bool indent)
{
_settings = settings;
_indent = indent;
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UnicodeEncodingWithBOM);
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json"));
}
public static JsonSerializerSettings CreateDefaultSettings()
{
return new JsonSerializerSettings()
{
MissingMemberHandling = MissingMemberHandling.Ignore,
// Do not change this setting
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types.
TypeNameHandling = TypeNameHandling.None
};
}
public void WriteObject([NotNull] TextWriter writer, object value)
{
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, value);
// We're explicitly calling flush here to simplify the debugging experience because the
// underlying TextWriter might be long-lived. If this method ends up being called repeatedly
// for a request, we should revisit.
jsonWriter.Flush();
}
}
private JsonWriter CreateJsonWriter(TextWriter writer)
{
var jsonWriter = new JsonTextWriter(writer);
if (_indent)
{
jsonWriter.Formatting = Formatting.Indented;
}
jsonWriter.CloseOutput = false;
return jsonWriter;
}
private JsonSerializer CreateJsonSerializer()
{
var jsonSerializer = JsonSerializer.Create(_settings);
return jsonSerializer;
}
public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
return SupportedMediaTypes.Any(supportedMediaType =>
contentType.RawValue.Equals(supportedMediaType.RawValue,
StringComparison.OrdinalIgnoreCase));
}
public override Task WriteAsync(OutputFormatterContext context,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var response = context.HttpContext.Response;
// The content type including the encoding should have been set already.
// In case it was not present, a default will be selected.
var selectedEncoding = SelectCharacterEncoding(MediaTypeHeaderValue.Parse(response.ContentType));
using (var writer = new StreamWriter(response.Body, selectedEncoding))
{
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, context.ObjectResult.Value);
// We're explicitly calling flush here to simplify the debugging experience because the
// underlying TextWriter might be long-lived. If this method ends up being called repeatedly
// for a request, we should revisit.
jsonWriter.Flush();
}
}
return Task.FromResult(true);
}
}
}

View File

@ -0,0 +1,103 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Writes an object to the output stream.
/// </summary>
public abstract class OutputFormatter
{
/// <summary>
/// Gets the mutable collection of character encodings supported by
/// this <see cref="OutputFormatter"/> instance. The encodings are
/// used when writing the data.
/// </summary>
public List<Encoding> SupportedEncodings { get; private set; }
/// <summary>
/// Gets the mutable collection of <see cref="MediaTypeHeaderValue"/> elements supported by
/// this <see cref="OutputFormatter"/> instance.
/// </summary>
public List<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="OutputFormatter"/> class.
/// </summary>
protected OutputFormatter()
{
SupportedEncodings = new List<Encoding>();
SupportedMediaTypes = new List<MediaTypeHeaderValue>();
}
/// <summary>
/// Determines the best <see cref="Encoding"/> amongst the supported encodings
/// for reading or writing an HTTP entity body based on the provided <paramref name="contentTypeHeader"/>.
/// </summary>
/// <param name="contentTypeHeader">The content type header provided as part of the request or response.</param>
/// <returns>The <see cref="Encoding"/> to use when reading the request or writing the response.</returns>
public virtual Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentTypeHeader)
{
Encoding encoding = null;
if (contentTypeHeader != null)
{
// Find encoding based on content type charset parameter
var charset = contentTypeHeader.Charset;
if (!String.IsNullOrWhiteSpace(charset))
{
encoding = SupportedEncodings.FirstOrDefault(
supportedEncoding =>
charset.Equals(supportedEncoding.WebName,
StringComparison.OrdinalIgnoreCase));
}
}
if (encoding == null)
{
// We didn't find a character encoding match based on the content headers.
// Instead we try getting the default character encoding.
if (SupportedEncodings.Count > 0)
{
encoding = SupportedEncodings[0];
}
}
if (encoding == null)
{
// No supported encoding was found so there is no way for us to start writing.
throw new InvalidOperationException(Resources.FormatOutputFormatterNoEncoding(GetType().FullName));
}
return encoding;
}
/// <summary>
/// Determines whether this <see cref="OutputFormatter"/> can serialize
/// an object of the specified type.
/// </summary>
/// <param name="context">The formatter context associated with the call</param>
/// <param name="contentType">The desired contentType on the response.</param>
/// <returns>True if this <see cref="OutputFormatter"/> is able to serialize the object
/// represent by <paramref name="context"/>'s ObjectResult and supports the passed in
/// <paramref name="contentType"/>.
/// False otherwise.</returns>
public abstract bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType);
/// <summary>
/// Writes given <paramref name="value"/> to the HttpResponse <paramref name="response"/> body stream.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A Task that serializes the value to the <paramref name="context"/>'s response message.</returns>
public abstract Task WriteAsync(OutputFormatterContext context, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,17 @@
// 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
{
public class OutputFormatterContext
{
public ObjectResult ObjectResult { get; set; }
public Type DeclaredType { get; set; }
public HttpContext HttpContext { get; set; }
}
}

View File

@ -1,65 +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.IO;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonOutputFormatter
{
private readonly JsonSerializerSettings _settings;
private readonly bool _indent;
public JsonOutputFormatter([NotNull] JsonSerializerSettings settings, bool indent)
{
_settings = settings;
_indent = indent;
}
public static JsonSerializerSettings CreateDefaultSettings()
{
return new JsonSerializerSettings()
{
MissingMemberHandling = MissingMemberHandling.Ignore,
// Do not change this setting
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types.
TypeNameHandling = TypeNameHandling.None
};
}
public void WriteObject([NotNull] TextWriter writer, object value)
{
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, value);
// We're explicitly calling flush here to simplify the debugging experience because the
// underlying TextWriter might be long-lived. If this method ends up being called repeatedly
// for a request, we should revisit.
jsonWriter.Flush();
}
}
private JsonWriter CreateJsonWriter([NotNull] TextWriter writer)
{
var jsonWriter = new JsonTextWriter(writer);
if (_indent)
{
jsonWriter.Formatting = Formatting.Indented;
}
jsonWriter.CloseOutput = false;
return jsonWriter;
}
private JsonSerializer CreateJsonSerializer()
{
var jsonSerializer = JsonSerializer.Create(_settings);
return jsonSerializer;
}
}
}

View File

@ -18,7 +18,9 @@
</PropertyGroup>
<ItemGroup>
<Content Include="project.json" />
<Content Include="Resources.resx" />
<Content Include="Resources.resx">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<Compile Include="AcceptVerbsAttribute.cs" />
@ -30,6 +32,9 @@
<Compile Include="ExpiringFileInfoCache.cs" />
<Compile Include="Extensions\ViewEngineDescriptorExtensions.cs" />
<Compile Include="IExpiringFileInfoCache.cs" />
<Compile Include="Formatters\OutputFormatterContext.cs" />
<Compile Include="Formatters\JsonOutputFormatter.cs" />
<Compile Include="Formatters\OutputFormatter.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />
<Compile Include="ReflectedActionDescriptorProvider.cs" />
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
@ -47,7 +52,7 @@
<Compile Include="ActionResults\HttpStatusCodeResult.cs" />
<Compile Include="ActionResults\JsonResult.cs" />
<Compile Include="ActionResults\NoContentResult.cs" />
<Compile Include="ActionResults\ObjectContentResult.cs" />
<Compile Include="ActionResults\ObjectResult.cs" />
<Compile Include="ActionResults\RedirectResult.cs" />
<Compile Include="ActionResults\RedirectToActionResult.cs" />
<Compile Include="ActionResults\RedirectToRouteResult.cs" />
@ -142,7 +147,6 @@
<Compile Include="Injector.cs" />
<Compile Include="Internal\TypeHelper.cs" />
<Compile Include="IUrlHelper.cs" />
<Compile Include="JsonOutputFormatter.cs" />
<Compile Include="MvcOptions.cs" />
<Compile Include="MvcRouteHandler.cs" />
<Compile Include="NonActionAttribute.cs" />

View File

@ -1050,6 +1050,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("TypeMustDeriveFromType"), p0, p1);
}
/// <summary>
/// No encoding found for output formatter '{0}'. There must be at least one supported encoding registered in order for the output formatter to write content.
/// </summary>
internal static string OutputFormatterNoEncoding
{
get { return GetString("OutputFormatterNoEncoding"); }
}
/// <summary>
/// No encoding found for output formatter '{0}'. There must be at least one supported encoding registered in order for the output formatter to write content.
/// </summary>
internal static string FormatOutputFormatterNoEncoding(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormatterNoEncoding"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Mvc
Resources.FormatActionResult_ActionReturnValueCannotBeNull(actualReturnType));
}
return new ObjectContentResult(actionReturnValue);
return new ObjectResult(actionReturnValue);
}
private IFilter[] GetFilters()

View File

@ -312,4 +312,7 @@
<data name="TypeMustDeriveFromType" xml:space="preserve">
<value>The type '{0}' must derive from '{1}'.</value>
</data>
<data name="OutputFormatterNoEncoding" xml:space="preserve">
<value>No encoding found for output formatter '{0}'. There must be at least one supported encoding registered in order for the output formatter to write content.</value>
</data>
</root>

View File

@ -6,6 +6,7 @@
"dependencies": {
"Microsoft.AspNet.FileSystems": "1.0.0-*",
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.Mvc.HeaderValueAbstractions": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": "",
"Microsoft.AspNet.Mvc.ModelBinding": "",
"Microsoft.AspNet.Routing": "1.0.0-*",

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
var actionContext = CreateMockActionContext();
// Act
var result = new ObjectContentResult(input);
var result = new ObjectResult(input);
// Assert
Assert.Equal(input, result.Value);
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
var actionContext = CreateMockActionContext(httpResponse.Object);
// Act
var result = new ObjectContentResult(input);
var result = new ObjectResult(input);
await result.ExecuteResultAsync(actionContext);
// Assert
@ -70,7 +70,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
}
// Act
var result = new ObjectContentResult(nonStringValue);
var result = new ObjectResult(nonStringValue);
await result.ExecuteResultAsync(actionContext);
// Assert

View File

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Xunit;
namespace Microsoft.AspNet.Mvc.Test
{
public class OutputFormatterTests
{
[Fact]
public void SelectCharacterEncoding_FormatterWithNoEncoding_Throws()
{
// Arrange
var testFormatter = new TestFormatter();
var testContentType = MediaTypeHeaderValue.Parse("text/invalid");
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => testFormatter.SelectCharacterEncoding(testContentType));
Assert.Equal("No encoding found for output formatter "+
"'Microsoft.AspNet.Mvc.Test.OutputFormatterTests+TestFormatter'." +
" There must be at least one supported encoding registered in order for the" +
" output formatter to write content.", ex.Message);
}
private class TestFormatter : OutputFormatter
{
public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
throw new NotImplementedException();
}
public override Task WriteAsync(OutputFormatterContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -33,6 +33,7 @@
<Compile Include="ExpiringFileInfoCacheTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsTests.cs" />
<Compile Include="Extensions\ViewEngineDscriptorExtensionsTest.cs" />
<Compile Include="Formatters\OutputFormatterTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedControllerModelTests.cs" />

View File

@ -1261,7 +1261,7 @@ namespace Microsoft.AspNet.Mvc
var actualResult = ReflectedActionInvoker.CreateActionResult(type, input);
// Assert
var contentResult = Assert.IsType<ObjectContentResult>(actualResult);
var contentResult = Assert.IsType<ObjectResult>(actualResult);
Assert.Same(input, contentResult.Value);
}

View File

@ -4,6 +4,7 @@
},
"dependencies": {
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.Mvc.HeaderValueAbstractions": "1.0.0-*",
"Microsoft.AspNet.Mvc" : "",
"Microsoft.AspNet.Mvc.Core" : "",
"Microsoft.AspNet.Mvc.ModelBinding": "",