From 1df4738a19f2dd75a590fe0698cc4574a6617132 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Fri, 11 Jul 2014 11:23:14 -0700 Subject: [PATCH] Adding OutputFormatter base class --- src/Microsoft.AspNet.Mvc.Common/Encodings.cs | 9 +- ...ObjectContentResult.cs => ObjectResult.cs} | 4 +- .../Formatters/JsonOutputFormatter.cs | 109 ++++++++++++++++++ .../Formatters/OutputFormatter.cs | 103 +++++++++++++++++ .../Formatters/OutputFormatterContext.cs | 17 +++ .../JsonOutputFormatter.cs | 65 ----------- .../Microsoft.AspNet.Mvc.Core.kproj | 10 +- .../Properties/Resources.Designer.cs | 16 +++ .../ReflectedActionInvoker.cs | 2 +- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 3 + src/Microsoft.AspNet.Mvc.Core/project.json | 1 + .../ActionResults/ObjectContentResultTests.cs | 6 +- .../Formatters/OutputFormatterTests.cs | 43 +++++++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + .../ReflectedActionInvokerTest.cs | 2 +- .../project.json | 1 + 16 files changed, 316 insertions(+), 76 deletions(-) rename src/Microsoft.AspNet.Mvc.Core/ActionResults/{ObjectContentResult.cs => ObjectResult.cs} (90%) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/JsonOutputFormatter.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs diff --git a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs index 7879febce1..714a78cd4f 100644 --- a/src/Microsoft.AspNet.Mvc.Common/Encodings.cs +++ b/src/Microsoft.AspNet.Mvc.Common/Encodings.cs @@ -8,9 +8,16 @@ namespace Microsoft.AspNet.Mvc internal static class Encodings { /// - /// Returns UTF8 Encoding without BOM and throws on invalid bytes + /// Returns UTF8 Encoding without BOM and throws on invalid bytes. /// public static readonly Encoding UTF8EncodingWithoutBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + /// + /// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes. + /// + public static readonly Encoding UnicodeEncodingWithBOM = new UnicodeEncoding(bigEndian: false, + byteOrderMark: true, + throwOnInvalidBytes: true); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectContentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs similarity index 90% rename from src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectContentResult.cs rename to src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs index eb95a3d317..c22fa073ab 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectContentResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs @@ -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; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs new file mode 100644 index 0000000000..238ff7e86c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs new file mode 100644 index 0000000000..2a4c7bb891 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -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 +{ + /// + /// Writes an object to the output stream. + /// + public abstract class OutputFormatter + { + /// + /// Gets the mutable collection of character encodings supported by + /// this instance. The encodings are + /// used when writing the data. + /// + public List SupportedEncodings { get; private set; } + + /// + /// Gets the mutable collection of elements supported by + /// this instance. + /// + public List SupportedMediaTypes { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + protected OutputFormatter() + { + SupportedEncodings = new List(); + SupportedMediaTypes = new List(); + } + + /// + /// Determines the best amongst the supported encodings + /// for reading or writing an HTTP entity body based on the provided . + /// + /// The content type header provided as part of the request or response. + /// The to use when reading the request or writing the response. + 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; + } + + /// + /// Determines whether this can serialize + /// an object of the specified type. + /// + /// The formatter context associated with the call + /// The desired contentType on the response. + /// True if this is able to serialize the object + /// represent by 's ObjectResult and supports the passed in + /// . + /// False otherwise. + public abstract bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType); + + /// + /// Writes given to the HttpResponse body stream. + /// + /// The token to monitor for cancellation requests. + /// A Task that serializes the value to the 's response message. + public abstract Task WriteAsync(OutputFormatterContext context, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs new file mode 100644 index 0000000000..217b908417 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/JsonOutputFormatter.cs deleted file mode 100644 index a5f7d1eea2..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/JsonOutputFormatter.cs +++ /dev/null @@ -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; - } - } -} 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 da3a641a4c..34b61069f0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -18,7 +18,9 @@ - + + Designer + @@ -30,6 +32,9 @@ + + + @@ -47,7 +52,7 @@ - + @@ -142,7 +147,6 @@ - diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 28e8b6f189..864be77022 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1050,6 +1050,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("TypeMustDeriveFromType"), p0, p1); } + /// + /// 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. + /// + internal static string OutputFormatterNoEncoding + { + get { return GetString("OutputFormatterNoEncoding"); } + } + + /// + /// 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. + /// + 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); diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs index a2c14c7fa9..17996810eb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs @@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Mvc Resources.FormatActionResult_ActionReturnValueCannotBeNull(actualReturnType)); } - return new ObjectContentResult(actionReturnValue); + return new ObjectResult(actionReturnValue); } private IFilter[] GetFilters() diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 63b76fcaa7..c73c9886e1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -312,4 +312,7 @@ The type '{0}' must derive from '{1}'. + + 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. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index d448274e9d..45e48450fe 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -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-*", diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs index cd48ac9ae6..30bd79d967 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs @@ -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 diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs new file mode 100644 index 0000000000..a8e85086b3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs @@ -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(() => 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(); + } + } + } +} 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 1152907613..5490f9bc70 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 @@ -33,6 +33,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs index 6d4f8c56dc..cc9a477b5d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs @@ -1261,7 +1261,7 @@ namespace Microsoft.AspNet.Mvc var actualResult = ReflectedActionInvoker.CreateActionResult(type, input); // Assert - var contentResult = Assert.IsType(actualResult); + var contentResult = Assert.IsType(actualResult); Assert.Same(input, contentResult.Value); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/project.json b/test/Microsoft.AspNet.Mvc.Core.Test/project.json index e614971c26..36d78ccc11 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Core.Test/project.json @@ -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": "",