[Fixes #4051] Split OutputFormatter into OutputFormatter and TextOutputFormatter
This commit is contained in:
parent
63354e25a8
commit
65858b8d8b
|
|
@ -9,7 +9,7 @@ using Microsoft.Net.Http.Headers;
|
|||
|
||||
namespace FormatFilterSample.Web
|
||||
{
|
||||
public class CustomFormatter : OutputFormatter
|
||||
public class CustomFormatter : TextOutputFormatter
|
||||
{
|
||||
public CustomFormatter(string contentType)
|
||||
{
|
||||
|
|
@ -38,7 +38,7 @@ namespace FormatFilterSample.Web
|
|||
return false;
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
await response.WriteAsync(context.Object.ToString());
|
||||
|
|
|
|||
|
|
@ -3,13 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -18,52 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// </summary>
|
||||
public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider
|
||||
{
|
||||
private IDictionary<string, string> _outputMediaTypeCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OutputFormatter"/> class.
|
||||
/// </summary>
|
||||
protected OutputFormatter()
|
||||
{
|
||||
SupportedEncodings = new List<Encoding>();
|
||||
SupportedMediaTypes = new MediaTypeCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable collection of character encodings supported by
|
||||
/// this <see cref="OutputFormatter"/>. The encodings are
|
||||
/// used when writing the data.
|
||||
/// </summary>
|
||||
public IList<Encoding> SupportedEncodings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable collection of media type elements supported by
|
||||
/// this <see cref="OutputFormatter"/>.
|
||||
/// </summary>
|
||||
public MediaTypeCollection SupportedMediaTypes { get; }
|
||||
|
||||
private IDictionary<string, string> OutputMediaTypeCache
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_outputMediaTypeCache == null)
|
||||
{
|
||||
var cache = new Dictionary<string, string>();
|
||||
foreach (var mediaType in SupportedMediaTypes)
|
||||
{
|
||||
cache.Add(mediaType, MediaType.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether or not the given type can be written by this serializer.
|
||||
/// </summary>
|
||||
|
|
@ -115,49 +79,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
/// <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="context">The formatter context associated with the call.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="Encoding"/> to use when reading the request or writing the response.</returns>
|
||||
public virtual Encoding SelectCharacterEncoding(OutputFormatterWriteContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var encoding = MatchAcceptCharacterEncoding(request.GetTypedHeaders().AcceptCharset);
|
||||
if (encoding != null)
|
||||
{
|
||||
return encoding;
|
||||
}
|
||||
|
||||
if (context.ContentType.HasValue)
|
||||
{
|
||||
var parsedContentType = new MediaType(context.ContentType);
|
||||
var contentTypeCharset = parsedContentType.Charset;
|
||||
if (contentTypeCharset.HasValue)
|
||||
{
|
||||
for (var i = 0; i < SupportedEncodings.Count; i++)
|
||||
{
|
||||
var supportedEncoding = SupportedEncodings[i];
|
||||
if (contentTypeCharset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This is supported.
|
||||
return SupportedEncodings[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A formatter for a non-text media-type won't have any supported encodings.
|
||||
return SupportedEncodings.Count > 0 ? SupportedEncodings[0] : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
{
|
||||
|
|
@ -206,46 +127,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(OutputFormatterWriteContext context)
|
||||
public virtual Task WriteAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var selectedMediaType = context.ContentType;
|
||||
if (!selectedMediaType.HasValue)
|
||||
{
|
||||
// If content type is not set then set it based on supported media types.
|
||||
if (SupportedEncodings.Count > 0)
|
||||
{
|
||||
selectedMediaType = new StringSegment(SupportedMediaTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatOutputFormatterNoMediaType(GetType().FullName));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Text-based media types will use an encoding/charset - binary formats just ignore it. We want to
|
||||
// make this class work with media types that use encodings, and those that don't.
|
||||
//
|
||||
// The default implementation of SelectCharacterEncoding will read from the list of SupportedEncodings
|
||||
// and will always choose a default encoding if any are supported. So, the only cases where the
|
||||
// selectedEncoding can be null are:
|
||||
//
|
||||
// 1). No supported encodings - we assume this is a non-text format
|
||||
// 2). Custom implementation of SelectCharacterEncoding - trust the user and give them what they want.
|
||||
var selectedEncoding = SelectCharacterEncoding(context);
|
||||
if (selectedEncoding != null)
|
||||
{
|
||||
// Override the content type value even if one already existed.
|
||||
var mediaTypeWithCharset = GetMediaTypeWithCharset(selectedMediaType.Value, selectedEncoding);
|
||||
selectedMediaType = new StringSegment(mediaTypeWithCharset);
|
||||
}
|
||||
|
||||
context.ContentType = selectedMediaType;
|
||||
|
||||
WriteResponseHeaders(context);
|
||||
return WriteResponseBodyAsync(context);
|
||||
}
|
||||
|
|
@ -271,100 +159,5 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// <param name="context">The formatter context associated with the call.</param>
|
||||
/// <returns>A task which can write the response body.</returns>
|
||||
public abstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context);
|
||||
|
||||
private string GetMediaTypeWithCharset(string mediaType, Encoding encoding)
|
||||
{
|
||||
if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) &&
|
||||
OutputMediaTypeCache.ContainsKey(mediaType))
|
||||
{
|
||||
return OutputMediaTypeCache[mediaType];
|
||||
}
|
||||
|
||||
return MediaType.ReplaceEncoding(mediaType, encoding);
|
||||
}
|
||||
|
||||
private Encoding MatchAcceptCharacterEncoding(IList<StringWithQualityHeaderValue> acceptCharsetHeaders)
|
||||
{
|
||||
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
|
||||
{
|
||||
var acceptValues = Sort(acceptCharsetHeaders);
|
||||
for (var i = 0; i < acceptValues.Count; i++)
|
||||
{
|
||||
var charset = acceptValues[i].Value;
|
||||
if (!string.IsNullOrEmpty(charset))
|
||||
{
|
||||
for (var j = 0; j < SupportedEncodings.Count; j++)
|
||||
{
|
||||
var encoding = SupportedEncodings[j];
|
||||
if (charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) ||
|
||||
charset.Equals("*", StringComparison.Ordinal))
|
||||
{
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// There's no allocation-free way to sort an IList and we may have to filter anyway,
|
||||
// so we're going to have to live with the copy + insertion sort.
|
||||
private IList<StringWithQualityHeaderValue> Sort(IList<StringWithQualityHeaderValue> values)
|
||||
{
|
||||
var sortNeeded = false;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
if (value.Quality == HeaderQuality.NoMatch)
|
||||
{
|
||||
// Exclude this one
|
||||
}
|
||||
else if (value.Quality != null)
|
||||
{
|
||||
count++;
|
||||
sortNeeded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sortNeeded)
|
||||
{
|
||||
return values;
|
||||
}
|
||||
|
||||
var sorted = new List<StringWithQualityHeaderValue>();
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
if (value.Quality == HeaderQuality.NoMatch)
|
||||
{
|
||||
// Exclude this one
|
||||
}
|
||||
else
|
||||
{
|
||||
// Doing an insertion sort.
|
||||
var position = sorted.BinarySearch(value, StringWithQualityHeaderValueComparer.QualityComparer);
|
||||
if (position >= 0)
|
||||
{
|
||||
sorted.Insert(position + 1, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sorted.Insert(~position, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We want a descending sort, but BinarySearch does ascending
|
||||
sorted.Reverse();
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// <summary>
|
||||
/// Always writes a string value to the response, regardless of requested content type.
|
||||
/// </summary>
|
||||
public class StringOutputFormatter : OutputFormatter
|
||||
public class StringOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public StringOutputFormatter()
|
||||
{
|
||||
|
|
@ -46,13 +46,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
return false;
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding encoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (encoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
var valueAsString = (string)context.Object;
|
||||
if (string.IsNullOrEmpty(valueAsString))
|
||||
{
|
||||
|
|
@ -60,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
return response.WriteAsync(valueAsString, MediaType.GetEncoding(response.ContentType) ?? Encoding.UTF8);
|
||||
return response.WriteAsync(valueAsString, encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
// Copyright (c) .NET Foundation. 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes an object in a given text format to the output stream.
|
||||
/// </summary>
|
||||
public abstract class TextOutputFormatter : OutputFormatter
|
||||
{
|
||||
private IDictionary<string, string> _outputMediaTypeCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextOutputFormatter"/> class.
|
||||
/// </summary>
|
||||
protected TextOutputFormatter()
|
||||
{
|
||||
SupportedEncodings = new List<Encoding>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable collection of character encodings supported by
|
||||
/// this <see cref="TextOutputFormatter"/>. The encodings are
|
||||
/// used when writing the data.
|
||||
/// </summary>
|
||||
public IList<Encoding> SupportedEncodings { get; }
|
||||
|
||||
private IDictionary<string, string> OutputMediaTypeCache
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_outputMediaTypeCache == null)
|
||||
{
|
||||
var cache = new Dictionary<string, string>();
|
||||
foreach (var mediaType in SupportedMediaTypes)
|
||||
{
|
||||
cache.Add(mediaType, MediaType.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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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="context">The formatter context associated with the call.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="Encoding"/> to use when reading the request or writing the response.</returns>
|
||||
public virtual Encoding SelectCharacterEncoding(OutputFormatterWriteContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (SupportedEncodings.Count == 0)
|
||||
{
|
||||
var message = Resources.FormatTextOutputFormatter_SupportedEncodingsMustNotBeEmpty(
|
||||
nameof(SupportedEncodings));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var encoding = MatchAcceptCharacterEncoding(request.GetTypedHeaders().AcceptCharset);
|
||||
if (encoding != null)
|
||||
{
|
||||
return encoding;
|
||||
}
|
||||
|
||||
if (context.ContentType.HasValue)
|
||||
{
|
||||
var parsedContentType = new MediaType(context.ContentType);
|
||||
var contentTypeCharset = parsedContentType.Charset;
|
||||
if (contentTypeCharset.HasValue)
|
||||
{
|
||||
for (var i = 0; i < SupportedEncodings.Count; i++)
|
||||
{
|
||||
var supportedEncoding = SupportedEncodings[i];
|
||||
if (contentTypeCharset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This is supported.
|
||||
return SupportedEncodings[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SupportedEncodings[0];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var selectedMediaType = context.ContentType;
|
||||
if (!selectedMediaType.HasValue)
|
||||
{
|
||||
// If content type is not set then set it based on supported media types.
|
||||
if (SupportedEncodings.Count > 0)
|
||||
{
|
||||
selectedMediaType = new StringSegment(SupportedMediaTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatOutputFormatterNoMediaType(GetType().FullName));
|
||||
}
|
||||
}
|
||||
|
||||
var selectedEncoding = SelectCharacterEncoding(context);
|
||||
if (selectedEncoding != null)
|
||||
{
|
||||
// Override the content type value even if one already existed.
|
||||
var mediaTypeWithCharset = GetMediaTypeWithCharset(selectedMediaType.Value, selectedEncoding);
|
||||
selectedMediaType = new StringSegment(mediaTypeWithCharset);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
context.ContentType = selectedMediaType;
|
||||
|
||||
WriteResponseHeaders(context);
|
||||
return WriteResponseBodyAsync(context, selectedEncoding);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
var message = Resources.FormatTextOutpurFormatter_WriteResponseBodyAsynNotSupported(
|
||||
$"{nameof(WriteResponseBodyAsync)}({nameof(OutputFormatterWriteContext)})",
|
||||
nameof(TextOutputFormatter),
|
||||
$"{nameof(WriteResponseBodyAsync)}({nameof(OutputFormatterWriteContext)},{nameof(Encoding)})");
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the response body.
|
||||
/// </summary>
|
||||
/// <param name="context">The formatter context associated with the call.</param>
|
||||
/// <param name="selectedEncoding">The <see cref="Encoding"/> that should be used to write the response.</param>
|
||||
/// <returns>A task which can write the response body.</returns>
|
||||
public abstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding);
|
||||
|
||||
private string GetMediaTypeWithCharset(string mediaType, Encoding encoding)
|
||||
{
|
||||
if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) &&
|
||||
OutputMediaTypeCache.ContainsKey(mediaType))
|
||||
{
|
||||
return OutputMediaTypeCache[mediaType];
|
||||
}
|
||||
|
||||
return MediaType.ReplaceEncoding(mediaType, encoding);
|
||||
}
|
||||
|
||||
private Encoding MatchAcceptCharacterEncoding(IList<StringWithQualityHeaderValue> acceptCharsetHeaders)
|
||||
{
|
||||
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
|
||||
{
|
||||
var acceptValues = Sort(acceptCharsetHeaders);
|
||||
for (var i = 0; i < acceptValues.Count; i++)
|
||||
{
|
||||
var charset = acceptValues[i].Value;
|
||||
if (!string.IsNullOrEmpty(charset))
|
||||
{
|
||||
for (var j = 0; j < SupportedEncodings.Count; j++)
|
||||
{
|
||||
var encoding = SupportedEncodings[j];
|
||||
if (charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) ||
|
||||
charset.Equals("*", StringComparison.Ordinal))
|
||||
{
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// There's no allocation-free way to sort an IList and we may have to filter anyway,
|
||||
// so we're going to have to live with the copy + insertion sort.
|
||||
private IList<StringWithQualityHeaderValue> Sort(IList<StringWithQualityHeaderValue> values)
|
||||
{
|
||||
var sortNeeded = false;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
if (value.Quality == HeaderQuality.NoMatch)
|
||||
{
|
||||
// Exclude this one
|
||||
}
|
||||
else if (value.Quality != null)
|
||||
{
|
||||
count++;
|
||||
sortNeeded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sortNeeded)
|
||||
{
|
||||
return values;
|
||||
}
|
||||
|
||||
var sorted = new List<StringWithQualityHeaderValue>();
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
if (value.Quality == HeaderQuality.NoMatch)
|
||||
{
|
||||
// Exclude this one
|
||||
}
|
||||
else
|
||||
{
|
||||
// Doing an insertion sort.
|
||||
var position = sorted.BinarySearch(value, StringWithQualityHeaderValueComparer.QualityComparer);
|
||||
if (position >= 0)
|
||||
{
|
||||
sorted.Insert(position + 1, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sorted.Insert(~position, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We want a descending sort, but BinarySearch does ascending
|
||||
sorted.Reverse();
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1114,6 +1114,38 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list of '{0}' must not be empty. Add at least one supported encoding.
|
||||
/// </summary>
|
||||
internal static string TextOutputFormatter_SupportedEncodingsMustNotBeEmpty
|
||||
{
|
||||
get { return GetString("TextOutputFormatter_SupportedEncodingsMustNotBeEmpty"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list of '{0}' must not be empty. Add at least one supported encoding.
|
||||
/// </summary>
|
||||
internal static string FormatTextOutputFormatter_SupportedEncodingsMustNotBeEmpty(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TextOutputFormatter_SupportedEncodingsMustNotBeEmpty"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' is not supported by '{1}'. Use '{2}' instead.
|
||||
/// </summary>
|
||||
internal static string TextOutpurFormatter_WriteResponseBodyAsynNotSupported
|
||||
{
|
||||
get { return GetString("TextOutpurFormatter_WriteResponseBodyAsynNotSupported"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' is not supported by '{1}'. Use '{2}' instead.
|
||||
/// </summary>
|
||||
internal static string FormatTextOutpurFormatter_WriteResponseBodyAsynNotSupported(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TextOutpurFormatter_WriteResponseBodyAsynNotSupported"), p0, p1, p2);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -334,4 +334,10 @@
|
|||
<data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
|
||||
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
|
||||
</data>
|
||||
<data name="TextOutputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
|
||||
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
|
||||
</data>
|
||||
<data name="TextOutpurFormatter_WriteResponseBodyAsynNotSupported" xml:space="preserve">
|
||||
<value>'{0}' is not supported by '{1}'. Use '{2}' instead.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// <summary>
|
||||
/// An output formatter that specializes in writing JSON content.
|
||||
/// </summary>
|
||||
public class JsonOutputFormatter : OutputFormatter
|
||||
public class JsonOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
private readonly IArrayPool<char> _charPool;
|
||||
|
||||
|
|
@ -83,6 +83,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the given <paramref name="value"/> as JSON using the given
|
||||
/// <paramref name="writer"/>.
|
||||
/// </summary>
|
||||
/// <param name="writer">The <see cref="TextWriter"/> used to write the <paramref name="value"/></param>
|
||||
/// <param name="value">The value to write as JSON.</param>
|
||||
public void WriteObject(TextWriter writer, object value)
|
||||
{
|
||||
if (writer == null)
|
||||
|
|
@ -132,16 +138,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
return _serializer;
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
var selectedEncoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
|
||||
if (selectedEncoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectedEncoding));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
|
||||
{
|
||||
WriteObject(writer, context.Object);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ using System.Threading.Tasks;
|
|||
using System.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -20,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// This class handles serialization of objects
|
||||
/// to XML using <see cref="DataContractSerializer"/>
|
||||
/// </summary>
|
||||
public class XmlDataContractSerializerOutputFormatter : OutputFormatter
|
||||
public class XmlDataContractSerializerOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
private DataContractSerializerSettings _serializerSettings;
|
||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||
|
|
@ -180,15 +178,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (selectedEncoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectedEncoding));
|
||||
}
|
||||
|
||||
var writerSettings = WriterSettings.Clone();
|
||||
writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
|
||||
writerSettings.Encoding = selectedEncoding;
|
||||
|
||||
// Wrap the object only if there is a wrapping type.
|
||||
var value = context.Object;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ using System.Xml;
|
|||
using System.Xml.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -20,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// This class handles serialization of objects
|
||||
/// to XML using <see cref="XmlSerializer"/>
|
||||
/// </summary>
|
||||
public class XmlSerializerOutputFormatter : OutputFormatter
|
||||
public class XmlSerializerOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||
|
||||
|
|
@ -155,17 +153,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (selectedEncoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectedEncoding));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
|
||||
var writerSettings = WriterSettings.Clone();
|
||||
writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
|
||||
writerSettings.Encoding = selectedEncoding;
|
||||
|
||||
// Wrap the object only if there is a wrapping type.
|
||||
var value = context.Object;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -28,17 +29,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.IsType(typeof(AnotherTestOutputFormatter), formatter);
|
||||
}
|
||||
|
||||
private class TestOutputFormatter : OutputFormatter
|
||||
private class TestOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class AnotherTestOutputFormatter : OutputFormatter
|
||||
private class AnotherTestOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,228 +18,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
{
|
||||
public class OutputFormatterTests
|
||||
{
|
||||
public static IEnumerable<object[]> SelectResponseCharacterEncodingData
|
||||
{
|
||||
get
|
||||
{
|
||||
// string acceptEncodings, string requestEncoding, string[] supportedEncodings, string expectedEncoding
|
||||
yield return new object[] { "", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
|
||||
yield return new object[] { "utf-8", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
yield return new object[] { "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
|
||||
yield return new object[] { "utf-16; q=0.5", new string[] { "utf-8", "utf-16" }, "utf-16" };
|
||||
|
||||
yield return new object[] { "utf-8; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
yield return new object[] { "utf-8; q=0.0, utf-16; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
|
||||
yield return new object[] { "*; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SelectResponseCharacterEncodingData))]
|
||||
public void SelectResponseCharacterEncoding_SelectsEncoding(
|
||||
string acceptCharsetHeaders,
|
||||
string[] supportedEncodings,
|
||||
string expectedEncoding)
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var httpRequest = new DefaultHttpContext().Request;
|
||||
httpRequest.Headers[HeaderNames.AcceptCharset] = acceptCharsetHeaders;
|
||||
httpRequest.Headers[HeaderNames.Accept] = "application/acceptCharset";
|
||||
httpContext.SetupGet(o => o.Request).Returns(httpRequest);
|
||||
|
||||
var formatter = new TestOutputFormatter();
|
||||
foreach (string supportedEncoding in supportedEncodings)
|
||||
{
|
||||
formatter.SupportedEncodings.Add(Encoding.GetEncoding(supportedEncoding));
|
||||
}
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
httpContext.Object,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
"someValue")
|
||||
{
|
||||
ContentType = new StringSegment(httpRequest.Headers[HeaderNames.Accept]),
|
||||
};
|
||||
|
||||
// Act
|
||||
var actualEncoding = formatter.SelectCharacterEncoding(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Encoding.GetEncoding(expectedEncoding), actualEncoding);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json; charset=utf-16", "application/json; charset=utf-32")]
|
||||
[InlineData("application/json; charset=utf-16; format=indent", "application/json; charset=utf-32; format=indent")]
|
||||
public void WriteResponse_OverridesCharset_IfDifferentFromContentTypeCharset(
|
||||
string contentType,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new Mock<OutputFormatter>();
|
||||
|
||||
formatter
|
||||
.Setup(f => f.SelectCharacterEncoding(It.IsAny<OutputFormatterWriteContext>()))
|
||||
.Returns(Encoding.UTF32);
|
||||
|
||||
var formatterContext = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
ContentType = new StringSegment(contentType),
|
||||
};
|
||||
|
||||
// Act
|
||||
formatter.Object.WriteAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestOutputFormatter();
|
||||
|
||||
var testContentType = new StringSegment("text/json");
|
||||
|
||||
formatter.SupportedEncodings.Clear();
|
||||
formatter.SupportedMediaTypes.Clear();
|
||||
formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
ContentType = testContentType,
|
||||
};
|
||||
|
||||
// Act
|
||||
formatter.WriteResponseHeaders(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(MediaTypeHeaderValue.Parse(context.ContentType.Value).Encoding);
|
||||
Assert.Equal(testContentType, context.ContentType);
|
||||
|
||||
// If we had set an encoding, it would be part of the content type header
|
||||
Assert.Equal(MediaTypeHeaderValue.Parse(testContentType.Value), context.HttpContext.Response.GetTypedHeaders().ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteResponseHeaders_ClonesMediaType()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new PngImageFormatter();
|
||||
formatter.SupportedMediaTypes.Clear();
|
||||
var mediaType = new MediaTypeHeaderValue("image/png");
|
||||
formatter.SupportedMediaTypes.Add(mediaType);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
// Act
|
||||
await formatter.WriteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(mediaType, context.ContentType);
|
||||
Assert.Null(mediaType.Charset);
|
||||
Assert.Equal("image/png; charset=utf-8", context.ContentType.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteResult_ForNullContentType_UsesFirstEntryInSupportedContentTypes()
|
||||
{
|
||||
|
|
@ -397,42 +175,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
{
|
||||
public TestOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/acceptCharset"));
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class DoesNotSetContext : OutputFormatter
|
||||
{
|
||||
public DoesNotSetContext()
|
||||
{
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/doesNotSetContext"));
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
}
|
||||
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
{
|
||||
// Do not set the selected media Type.
|
||||
// The WriteResponseHeaders should do it for you.
|
||||
return true;
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class PngImageFormatter : OutputFormatter
|
||||
{
|
||||
public PngImageFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("image/png"));
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedMediaTypes.Add("application/acceptCharset");
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Encoding encoding = Encoding.UTF8;
|
||||
var memoryStream = new MemoryStream();
|
||||
var response = new Mock<HttpResponse>();
|
||||
response.SetupProperty<long?>(o => o.ContentLength);
|
||||
response.SetupProperty(o => o.ContentLength);
|
||||
response.SetupGet(r => r.Body).Returns(memoryStream);
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
httpContext.Setup(o => o.Response).Returns(response.Object);
|
||||
|
|
@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
@object: null);
|
||||
|
||||
// Act
|
||||
await formatter.WriteResponseBodyAsync(context);
|
||||
await formatter.WriteResponseBodyAsync(context, encoding);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, memoryStream.Length);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
// Copyright (c) .NET Foundation. 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class TextOutputFormatterTests
|
||||
{
|
||||
public static IEnumerable<object[]> SelectResponseCharacterEncodingData
|
||||
{
|
||||
get
|
||||
{
|
||||
// string acceptEncodings, string requestEncoding, string[] supportedEncodings, string expectedEncoding
|
||||
yield return new object[] { "", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
|
||||
yield return new object[] { "utf-8", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
yield return new object[] { "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
|
||||
yield return new object[] { "utf-16; q=0.5", new string[] { "utf-8", "utf-16" }, "utf-16" };
|
||||
|
||||
yield return new object[] { "utf-8; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
yield return new object[] { "utf-8; q=0.0, utf-16; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
|
||||
yield return new object[] { "*; q=0.0", new string[] { "utf-8", "utf-16" }, "utf-8" };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SelectResponseCharacterEncodingData))]
|
||||
public void SelectResponseCharacterEncoding_SelectsEncoding(
|
||||
string acceptCharsetHeaders,
|
||||
string[] supportedEncodings,
|
||||
string expectedEncoding)
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var httpRequest = new DefaultHttpContext().Request;
|
||||
httpRequest.Headers[HeaderNames.AcceptCharset] = acceptCharsetHeaders;
|
||||
httpRequest.Headers[HeaderNames.Accept] = "application/acceptCharset";
|
||||
httpContext.SetupGet(o => o.Request).Returns(httpRequest);
|
||||
|
||||
var formatter = new TestOutputFormatter();
|
||||
foreach (string supportedEncoding in supportedEncodings)
|
||||
{
|
||||
formatter.SupportedEncodings.Add(Encoding.GetEncoding(supportedEncoding));
|
||||
}
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
httpContext.Object,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
"someValue")
|
||||
{
|
||||
ContentType = new StringSegment(httpRequest.Headers[HeaderNames.Accept]),
|
||||
};
|
||||
|
||||
// Act
|
||||
var actualEncoding = formatter.SelectCharacterEncoding(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Encoding.GetEncoding(expectedEncoding), actualEncoding);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json; charset=utf-16", "application/json; charset=utf-32")]
|
||||
[InlineData("application/json; charset=utf-16; format=indent", "application/json; charset=utf-32; format=indent")]
|
||||
public void WriteResponse_OverridesCharset_IfDifferentFromContentTypeCharset(
|
||||
string contentType,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new OverrideEncodingFormatter(Encoding.UTF32);
|
||||
|
||||
var formatterContext = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
ContentType = new StringSegment(contentType),
|
||||
};
|
||||
|
||||
// Act
|
||||
formatter.WriteAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestOutputFormatter();
|
||||
|
||||
var testContentType = new StringSegment("text/json");
|
||||
|
||||
formatter.SupportedEncodings.Clear();
|
||||
formatter.SupportedMediaTypes.Clear();
|
||||
formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
ContentType = testContentType,
|
||||
};
|
||||
|
||||
// Act
|
||||
formatter.WriteResponseHeaders(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(MediaTypeHeaderValue.Parse(context.ContentType.Value).Encoding);
|
||||
Assert.Equal(testContentType, context.ContentType);
|
||||
|
||||
// If we had set an encoding, it would be part of the content type header
|
||||
Assert.Equal(MediaTypeHeaderValue.Parse(testContentType.Value), context.HttpContext.Response.GetTypedHeaders().ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_ReturnsNotAcceptable_IfSelectCharacterEncodingReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new OverrideEncodingFormatter(encoding: null);
|
||||
|
||||
var testContentType = new StringSegment("text/json");
|
||||
formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
ContentType = testContentType,
|
||||
};
|
||||
|
||||
// Act
|
||||
await formatter.WriteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status406NotAcceptable, context.HttpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
private class TestOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add("application/acceptCharset");
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class OverrideEncodingFormatter : TextOutputFormatter
|
||||
{
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public OverrideEncodingFormatter(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public override Encoding SelectCharacterEncoding(OutputFormatterWriteContext context)
|
||||
{
|
||||
return _encoding;
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -520,7 +520,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private class TestJsonOutputFormatter : OutputFormatter
|
||||
private class TestJsonOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestJsonOutputFormatter()
|
||||
{
|
||||
|
|
@ -530,13 +530,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestXmlOutputFormatter : OutputFormatter
|
||||
private class TestXmlOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestXmlOutputFormatter()
|
||||
{
|
||||
|
|
@ -546,13 +546,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestStringOutputFormatter : OutputFormatter
|
||||
private class TestStringOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestStringOutputFormatter()
|
||||
{
|
||||
|
|
@ -561,7 +561,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User));
|
||||
|
||||
// Act
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext.HttpContext.Response.Body;
|
||||
|
|
@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
// This will create a serializer - which gets cached.
|
||||
var outputFormatterContext1 = GetOutputFormatterContext(person, typeof(User));
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext1);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext1, Encoding.UTF8);
|
||||
|
||||
// These changes should have no effect.
|
||||
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var outputFormatterContext2 = GetOutputFormatterContext(person, typeof(User));
|
||||
|
||||
// Act
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext2);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext2, Encoding.UTF8);
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext2.HttpContext.Response.Body;
|
||||
|
|
@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
// This will create a serializer - which gets cached.
|
||||
var outputFormatterContext1 = GetOutputFormatterContext(person, typeof(User));
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext1);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext1, Encoding.UTF8);
|
||||
|
||||
// This results in a new serializer being created.
|
||||
jsonFormatter.SerializerSettings = new JsonSerializerSettings()
|
||||
|
|
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var outputFormatterContext2 = GetOutputFormatterContext(person, typeof(User));
|
||||
|
||||
// Act
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext2);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext2, Encoding.UTF8);
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext2.HttpContext.Response.Body;
|
||||
|
|
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var outputFormatterContext = GetOutputFormatterContext(person, typeof(User));
|
||||
|
||||
// Act
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext);
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext.HttpContext.Response.Body;
|
||||
|
|
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
memStream);
|
||||
|
||||
// Act
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext);
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
||||
|
||||
// Assert
|
||||
memStream.Position = 0;
|
||||
|
|
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
};
|
||||
|
||||
// Act
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext);
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));
|
||||
|
||||
// Assert
|
||||
var actualData = body.ToArray();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using Microsoft.Net.Http.Headers;
|
|||
|
||||
namespace BasicWebSite.Formatters
|
||||
{
|
||||
public class CustomFormatter : OutputFormatter
|
||||
public class CustomFormatter : TextOutputFormatter
|
||||
{
|
||||
public string ContentType { get; private set; }
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ namespace BasicWebSite.Formatters
|
|||
return false;
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = ContentType + ";charset=utf-8";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using Microsoft.Net.Http.Headers;
|
|||
|
||||
namespace BasicWebSite.Formatters
|
||||
{
|
||||
public class PlainTextFormatter : OutputFormatter
|
||||
public class PlainTextFormatter : TextOutputFormatter
|
||||
{
|
||||
public PlainTextFormatter()
|
||||
{
|
||||
|
|
@ -31,7 +31,7 @@ namespace BasicWebSite.Formatters
|
|||
return false;
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = "text/plain;charset=utf-8";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace BasicWebSite.Formatters
|
|||
/// <summary>
|
||||
/// Provides contact information of a person through VCard format.
|
||||
/// </summary>
|
||||
public class VCardFormatter_V3 : OutputFormatter
|
||||
public class VCardFormatter_V3 : TextOutputFormatter
|
||||
{
|
||||
public VCardFormatter_V3()
|
||||
{
|
||||
|
|
@ -28,7 +28,7 @@ namespace BasicWebSite.Formatters
|
|||
return typeof(Contact).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
var contact = (Contact)context.Object;
|
||||
|
||||
|
|
@ -38,8 +38,6 @@ namespace BasicWebSite.Formatters
|
|||
builder.AppendLine();
|
||||
builder.AppendLine("END:VCARD");
|
||||
|
||||
var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8;
|
||||
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
builder.ToString(),
|
||||
selectedEncoding);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace BasicWebSite.Formatters
|
|||
/// Provides contact information of a person through VCard format.
|
||||
/// In version 4.0 of VCard format, Gender is a supported property.
|
||||
/// </summary>
|
||||
public class VCardFormatter_V4 : OutputFormatter
|
||||
public class VCardFormatter_V4 : TextOutputFormatter
|
||||
{
|
||||
public VCardFormatter_V4()
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@ namespace BasicWebSite.Formatters
|
|||
return typeof(Contact).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
var contact = (Contact)context.Object;
|
||||
|
||||
|
|
@ -41,8 +41,6 @@ namespace BasicWebSite.Formatters
|
|||
builder.AppendLine();
|
||||
builder.AppendLine("END:VCARD");
|
||||
|
||||
var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8;
|
||||
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
builder.ToString(),
|
||||
selectedEncoding);
|
||||
|
|
|
|||
Loading…
Reference in New Issue