diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs index a8da6ae7a1..c516f92f49 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs @@ -69,8 +69,13 @@ namespace Microsoft.AspNet.Mvc.Formatters public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding) { var parsedMediaType = MediaTypeHeaderValue.Parse(mediaType.Value); - parsedMediaType.Encoding = encoding; + if (string.Equals(parsedMediaType.Encoding?.WebName, encoding?.WebName, StringComparison.OrdinalIgnoreCase)) + { + return mediaType.Value; + } + + parsedMediaType.Encoding = encoding; return parsedMediaType.ToString(); } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs index 41a8d9e57d..9d538122ef 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNet.Mvc.Formatters /// public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider { + private IDictionary _outputMediaTypeCache; + /// /// Initializes a new instance of the class. /// @@ -40,6 +42,28 @@ namespace Microsoft.AspNet.Mvc.Formatters /// public MediaTypeCollection SupportedMediaTypes { get; } + private IDictionary OutputMediaTypeCache + { + get + { + if (_outputMediaTypeCache == null) + { + var cache = new Dictionary(); + foreach (var mediaType in SupportedMediaTypes) + { + cache.Add(mediaType, MediaTypeEncoding.ReplaceEncoding(mediaType, Encoding.UTF8)); + } + + // Safe race condition, worst case scenario we initialize the field multiple times with dictionaries containing + // the same values. + _outputMediaTypeCache = cache; + } + + return _outputMediaTypeCache; + } + } + + /// /// Returns a value indicating whether or not the given type can be written by this serializer. /// @@ -244,31 +268,15 @@ namespace Microsoft.AspNet.Mvc.Formatters /// A task which can write the response body. public abstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context); - /// - /// Adds or replaces the charset parameter in a given with the - /// given . - /// - /// The with the media type. - /// - /// The to add or replace in the . - /// - /// The mediaType with the given encoding. - protected string GetMediaTypeWithCharset(string mediaType, Encoding encoding) + private string GetMediaTypeWithCharset(string mediaType, Encoding encoding) { - var mediaTypeEncoding = MediaTypeEncoding.GetEncoding(mediaType); - if (mediaTypeEncoding == encoding) + if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) && + OutputMediaTypeCache.ContainsKey(mediaType)) { - return mediaType; - } - else if (mediaTypeEncoding == null) - { - return CreateMediaTypeWithEncoding(mediaType, encoding); - } - else - { - // This can happen if the user has overriden SelectCharacterEncoding - return MediaTypeEncoding.ReplaceEncoding(mediaType, encoding); + return OutputMediaTypeCache[mediaType]; } + + return MediaTypeEncoding.ReplaceEncoding(mediaType, encoding); } private Encoding MatchAcceptCharacterEncoding(IList acceptCharsetHeaders) @@ -354,15 +362,5 @@ namespace Microsoft.AspNet.Mvc.Formatters sorted.Reverse(); return sorted; } - - private static string CreateMediaTypeWithEncoding(string mediaType, Encoding encoding) - { - return CreateMediaTypeWithEncoding(new StringSegment(mediaType), encoding); - } - - private static string CreateMediaTypeWithEncoding(StringSegment mediaType, Encoding encoding) - { - return $"{mediaType.Value}; charset={encoding.WebName}"; - } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs index 8a4aebb122..c796024ea4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.TestCommon; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; @@ -101,6 +102,88 @@ namespace Microsoft.AspNet.Mvc.Formatters Assert.Equal(new StringSegment(expectedContentType), formatterContext.ContentType); } + [Fact] + public void WriteResponse_GetMediaTypeWithCharsetReturnsMediaTypeFromCache_IfEncodingIsUtf8() + { + // Arrange + var formatter = new TestOutputFormatter(); + + var formatterContext = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null) + { + ContentType = new StringSegment("application/json"), + }; + + formatter.SupportedMediaTypes.Add("application/json"); + formatter.SupportedEncodings.Add(Encoding.UTF8); + + // Act + formatter.WriteAsync(formatterContext); + var firstContentType = formatterContext.ContentType; + + formatterContext.ContentType = new StringSegment("application/json"); + + formatter.WriteAsync(formatterContext); + var secondContentType = formatterContext.ContentType; + + // Assert + Assert.Same(firstContentType.Buffer, secondContentType.Buffer); + } + + [Fact] + public void WriteResponse_GetMediaTypeWithCharsetReplacesCharset_IfDifferentThanEncoding() + { + // Arrange + var formatter = new TestOutputFormatter(); + + var formatterContext = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null) + { + ContentType = new StringSegment("application/json; charset=utf-7"), + }; + + formatter.SupportedMediaTypes.Add("application/json"); + formatter.SupportedEncodings.Add(Encoding.UTF8); + + // Act + formatter.WriteAsync(formatterContext); + + // Assert + Assert.Equal(new StringSegment("application/json; charset=utf-8"), formatterContext.ContentType); + } + + [Fact] + public void WriteResponse_GetMediaTypeWithCharsetReturnsSameString_IfCharsetEqualToEncoding() + { + // Arrange + var formatter = new TestOutputFormatter(); + + var contentType = "application/json; charset=utf-16"; + var formatterContext = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null) + { + ContentType = new StringSegment(contentType), + }; + + formatter.SupportedMediaTypes.Add("application/json"); + formatter.SupportedEncodings.Add(Encoding.Unicode); + + // Act + formatter.WriteAsync(formatterContext); + + // Assert + Assert.Same(contentType, formatterContext.ContentType.Buffer); + } + [Fact] public void WriteResponseContentHeaders_NoSupportedEncodings_NoEncodingIsSet() {