Refactor content negotiation code into a service (#6998)
* Refactor content negotiation code into a service This is a refactor in anticipation of using this logic in some other places
This commit is contained in:
parent
41efa409a4
commit
861d78fb78
|
|
@ -250,6 +250,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
|
||||
services.TryAddSingleton(ArrayPool<byte>.Shared);
|
||||
services.TryAddSingleton(ArrayPool<char>.Shared);
|
||||
services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();
|
||||
services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
|
||||
services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
|
||||
services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,322 @@
|
|||
// 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.Collections.ObjectModel;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class DefaultOutputFormatterSelector : OutputFormatterSelector
|
||||
{
|
||||
private static readonly Comparison<MediaTypeSegmentWithQuality> _sortFunction = (left, right) =>
|
||||
{
|
||||
return left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1);
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IList<IOutputFormatter> _formatters;
|
||||
private readonly bool _respectBrowserAcceptHeader;
|
||||
private readonly bool _returnHttpNotAcceptable;
|
||||
|
||||
public DefaultOutputFormatterSelector(IOptions<MvcOptions> options, ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_logger = loggerFactory.CreateLogger<DefaultOutputFormatterSelector>();
|
||||
|
||||
_formatters = new ReadOnlyCollection<IOutputFormatter>(options.Value.OutputFormatters);
|
||||
_respectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
|
||||
_returnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
|
||||
}
|
||||
|
||||
public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
if (contentTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contentTypes));
|
||||
}
|
||||
|
||||
ValidateContentTypes(contentTypes);
|
||||
|
||||
if (formatters.Count == 0)
|
||||
{
|
||||
formatters = _formatters;
|
||||
if (formatters.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
|
||||
typeof(MvcOptions).FullName,
|
||||
nameof(MvcOptions.OutputFormatters),
|
||||
typeof(IOutputFormatter).FullName));
|
||||
}
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var acceptableMediaTypes = GetAcceptableMediaTypes(request);
|
||||
var selectFormatterWithoutRegardingAcceptHeader = false;
|
||||
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
if (acceptableMediaTypes.Count == 0)
|
||||
{
|
||||
// There is either no Accept header value, or it contained */* and we
|
||||
// are not currently respecting the 'browser accept header'.
|
||||
_logger.NoAcceptForNegotiation();
|
||||
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
// Use whatever formatter can meet the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
context,
|
||||
formatters,
|
||||
acceptableMediaTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Verify that a content type from the context is compatible with the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
context,
|
||||
formatters,
|
||||
acceptableMediaTypes,
|
||||
contentTypes);
|
||||
}
|
||||
|
||||
if (selectedFormatter == null && !_returnHttpNotAcceptable)
|
||||
{
|
||||
_logger.NoFormatterFromNegotiation(acceptableMediaTypes);
|
||||
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectFormatterWithoutRegardingAcceptHeader)
|
||||
{
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
selectedFormatter = SelectFormatterNotUsingContentType(
|
||||
context,
|
||||
formatters);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
|
||||
context,
|
||||
formatters,
|
||||
contentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// No formatter supports this.
|
||||
_logger.NoFormatter(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.FormatterSelected(selectedFormatter, context);
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(HttpRequest request)
|
||||
{
|
||||
var result = new List<MediaTypeSegmentWithQuality>();
|
||||
AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result);
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
var mediaType = new MediaType(result[i].MediaType);
|
||||
if (!_respectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
|
||||
{
|
||||
result.Clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.Sort(_sortFunction);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IOutputFormatter SelectFormatterNotUsingContentType(
|
||||
OutputFormatterCanWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
formatterContext.ContentType = new StringSegment();
|
||||
formatterContext.ContentTypeIsServerDefined = false;
|
||||
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
|
||||
OutputFormatterCanWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
IList<MediaTypeSegmentWithQuality> sortedAcceptHeaders)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
if (sortedAcceptHeaders == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sortedAcceptHeaders));
|
||||
}
|
||||
|
||||
for (var i = 0; i < sortedAcceptHeaders.Count; i++)
|
||||
{
|
||||
var mediaType = sortedAcceptHeaders[i];
|
||||
|
||||
formatterContext.ContentType = mediaType.MediaType;
|
||||
formatterContext.ContentTypeIsServerDefined = false;
|
||||
|
||||
for (var j = 0; j < formatters.Count; j++)
|
||||
{
|
||||
var formatter = formatters[j];
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
|
||||
OutputFormatterCanWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
MediaTypeCollection acceptableContentTypes)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
if (acceptableContentTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(acceptableContentTypes));
|
||||
}
|
||||
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
foreach (var contentType in acceptableContentTypes)
|
||||
{
|
||||
formatterContext.ContentType = new StringSegment(contentType);
|
||||
formatterContext.ContentTypeIsServerDefined = true;
|
||||
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
OutputFormatterCanWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
|
||||
MediaTypeCollection possibleOutputContentTypes)
|
||||
{
|
||||
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
|
||||
{
|
||||
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
|
||||
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
|
||||
{
|
||||
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
|
||||
if (candidateContentType.IsSubsetOf(acceptableContentType))
|
||||
{
|
||||
for (var k = 0; k < formatters.Count; k++)
|
||||
{
|
||||
var formatter = formatters[k];
|
||||
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
|
||||
formatterContext.ContentTypeIsServerDefined = true;
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ValidateContentTypes(MediaTypeCollection contentTypes)
|
||||
{
|
||||
for (var i = 0; i < contentTypes.Count; i++)
|
||||
{
|
||||
var contentType = contentTypes[i];
|
||||
|
||||
var parsedContentType = new MediaType(contentType);
|
||||
if (parsedContentType.HasWildcard)
|
||||
{
|
||||
var message = Resources.FormatObjectResult_MatchAllContentType(
|
||||
contentType,
|
||||
nameof(ObjectResult.ContentTypes));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
|
|
@ -27,17 +21,22 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectResultExecutor"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">An accessor to <see cref="MvcOptions"/>.</param>
|
||||
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
|
||||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public ObjectResultExecutor(
|
||||
IOptions<MvcOptions> options,
|
||||
OutputFormatterSelector formatterSelector,
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
if (formatterSelector == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
throw new ArgumentNullException(nameof(formatterSelector));
|
||||
}
|
||||
|
||||
if (writerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writerFactory));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
|
|
@ -45,11 +44,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
OptionsFormatters = options.Value.OutputFormatters;
|
||||
RespectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
|
||||
ReturnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
|
||||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
|
||||
FormatterSelector = formatterSelector;
|
||||
WriterFactory = writerFactory.CreateWriter;
|
||||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -58,19 +55,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <see cref="IOutputFormatter"/> instances from <see cref="MvcOptions"/>.
|
||||
/// Gets the <see cref="OutputFormatterSelector"/>.
|
||||
/// </summary>
|
||||
protected FormatterCollection<IOutputFormatter> OptionsFormatters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of <see cref="MvcOptions.RespectBrowserAcceptHeader"/>.
|
||||
/// </summary>
|
||||
protected bool RespectBrowserAcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of <see cref="MvcOptions.ReturnHttpNotAcceptable"/>.
|
||||
/// </summary>
|
||||
protected bool ReturnHttpNotAcceptable { get; }
|
||||
protected OutputFormatterSelector FormatterSelector { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writer factory delegate.
|
||||
|
|
@ -113,24 +100,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
ValidateContentTypes(result.ContentTypes);
|
||||
|
||||
var formatters = result.Formatters;
|
||||
if (formatters == null || formatters.Count == 0)
|
||||
{
|
||||
formatters = OptionsFormatters;
|
||||
|
||||
// Complain about MvcOptions.OutputFormatters only if the result has an empty Formatters.
|
||||
Debug.Assert(formatters != null, "MvcOptions.OutputFormatters cannot be null.");
|
||||
if (formatters.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
|
||||
typeof(MvcOptions).FullName,
|
||||
nameof(MvcOptions.OutputFormatters),
|
||||
typeof(IOutputFormatter).FullName));
|
||||
}
|
||||
}
|
||||
|
||||
var objectType = result.DeclaredType;
|
||||
if (objectType == null || objectType == typeof(object))
|
||||
{
|
||||
|
|
@ -143,7 +112,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
objectType,
|
||||
result.Value);
|
||||
|
||||
var selectedFormatter = SelectFormatter(formatterContext, result.ContentTypes, formatters);
|
||||
var selectedFormatter = FormatterSelector.SelectFormatter(
|
||||
formatterContext,
|
||||
(IList<IOutputFormatter>)result.Formatters ?? Array.Empty<IOutputFormatter>(),
|
||||
result.ContentTypes);
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// No formatter supports this.
|
||||
|
|
@ -153,334 +125,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Logger.FormatterSelected(selectedFormatter, formatterContext);
|
||||
Logger.ObjectResultExecuting(context);
|
||||
|
||||
result.OnFormatting(context);
|
||||
return selectedFormatter.WriteAsync(formatterContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="contentTypes">
|
||||
/// The list of content types provided by <see cref="ObjectResult.ContentTypes"/>.
|
||||
/// </param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatter(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
MediaTypeCollection contentTypes,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (contentTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contentTypes));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
var request = formatterContext.HttpContext.Request;
|
||||
var acceptableMediaTypes = GetAcceptableMediaTypes(request);
|
||||
var selectFormatterWithoutRegardingAcceptHeader = false;
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
|
||||
if (acceptableMediaTypes.Count == 0)
|
||||
{
|
||||
// There is either no Accept header value, or it contained */* and we
|
||||
// are not currently respecting the 'browser accept header'.
|
||||
Logger.NoAcceptForNegotiation();
|
||||
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
// Use whatever formatter can meet the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
acceptableMediaTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Verify that a content type from the context is compatible with the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
formatterContext,
|
||||
formatters,
|
||||
acceptableMediaTypes,
|
||||
contentTypes);
|
||||
}
|
||||
|
||||
if (selectedFormatter == null && !ReturnHttpNotAcceptable)
|
||||
{
|
||||
Logger.NoFormatterFromNegotiation(acceptableMediaTypes);
|
||||
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectFormatterWithoutRegardingAcceptHeader)
|
||||
{
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
selectedFormatter = SelectFormatterNotUsingContentType(
|
||||
formatterContext,
|
||||
formatters);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
|
||||
formatterContext,
|
||||
formatters,
|
||||
contentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(
|
||||
HttpRequest request)
|
||||
{
|
||||
var result = new List<MediaTypeSegmentWithQuality>();
|
||||
AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result);
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
var mediaType = new MediaType(result[i].MediaType);
|
||||
if (!RespectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
|
||||
{
|
||||
result.Clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.Sort((left, right) => left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response. The first formatter which
|
||||
/// can write the response should be chosen without any consideration for content type.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterNotUsingContentType(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
formatterContext.ContentType = new StringSegment();
|
||||
formatterContext.ContentTypeIsServerDefined = false;
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
|
||||
/// present in <paramref name="sortedAcceptHeaders"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <param name="sortedAcceptHeaders">
|
||||
/// The ordered content types from the <c>Accept</c> header, sorted by descending q-value.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
IList<MediaTypeSegmentWithQuality> sortedAcceptHeaders)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
if (sortedAcceptHeaders == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sortedAcceptHeaders));
|
||||
}
|
||||
|
||||
for (var i = 0; i < sortedAcceptHeaders.Count; i++)
|
||||
{
|
||||
var mediaType = sortedAcceptHeaders[i];
|
||||
formatterContext.ContentType = mediaType.MediaType;
|
||||
formatterContext.ContentTypeIsServerDefined = false;
|
||||
for (var j = 0; j < formatters.Count; j++)
|
||||
{
|
||||
var formatter = formatters[j];
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
|
||||
/// present in <paramref name="acceptableContentTypes"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <param name="acceptableContentTypes">
|
||||
/// The ordered content types from <see cref="ObjectResult.ContentTypes"/> in descending priority order.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
MediaTypeCollection acceptableContentTypes)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
if (acceptableContentTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(acceptableContentTypes));
|
||||
}
|
||||
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
foreach (var contentType in acceptableContentTypes)
|
||||
{
|
||||
formatterContext.ContentType = new StringSegment(contentType);
|
||||
formatterContext.ContentTypeIsServerDefined = true;
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
|
||||
/// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <param name="sortedAcceptableContentTypes">
|
||||
/// The ordered content types from the <c>Accept</c> header, sorted by descending q-value.
|
||||
/// </param>
|
||||
/// <param name="possibleOutputContentTypes">
|
||||
/// The ordered content types from <see cref="ObjectResult.ContentTypes"/> in descending priority order.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
|
||||
MediaTypeCollection possibleOutputContentTypes)
|
||||
{
|
||||
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
|
||||
{
|
||||
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
|
||||
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
|
||||
{
|
||||
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
|
||||
if (candidateContentType.IsSubsetOf(acceptableContentType))
|
||||
{
|
||||
for (var k = 0; k < formatters.Count; k++)
|
||||
{
|
||||
var formatter = formatters[k];
|
||||
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
|
||||
formatterContext.ContentTypeIsServerDefined = true;
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ValidateContentTypes(MediaTypeCollection contentTypes)
|
||||
{
|
||||
if (contentTypes == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < contentTypes.Count; i++)
|
||||
{
|
||||
var contentType = contentTypes[i];
|
||||
var parsedContentType = new MediaType(contentType);
|
||||
if (parsedContentType.HasWildcard)
|
||||
{
|
||||
var message = Resources.FormatObjectResult_MatchAllContentType(
|
||||
contentType,
|
||||
nameof(ObjectResult.ContentTypes));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects an <see cref="IOutputFormatter"/> to write a response to the current request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default implementation of <see cref="OutputFormatterSelector"/> provided by ASP.NET Core MVC
|
||||
/// is <see cref="DefaultOutputFormatterSelector"/>. The <see cref="DefaultOutputFormatterSelector"/> implements
|
||||
/// MVC's default content negotiation algorthm. This API is designed in a way that can satisfy the contract
|
||||
/// of <see cref="ObjectResult"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation is controlled by settings on <see cref="MvcOptions"/>, most notably:
|
||||
/// <see cref="MvcOptions.OutputFormatters"/>, <see cref="MvcOptions.RespectBrowserAcceptHeader"/>, and
|
||||
/// <see cref="MvcOptions.ReturnHttpNotAcceptable"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class OutputFormatterSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects an <see cref="IOutputFormatter"/> to write the response based on the provided values and the current request.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="OutputFormatterCanWriteContext"/> associated with the current request.</param>
|
||||
/// <param name="formatters">A list of formatters to use; this acts as an override to <see cref="MvcOptions.OutputFormatters"/>.</param>
|
||||
/// <param name="mediaTypes">A list of media types to use; this acts as an override to the <c>Accept</c> header. </param>
|
||||
/// <returns>The selected <see cref="IOutputFormatter"/>, or <c>null</c> if one could not be selected.</returns>
|
||||
public abstract IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection mediaTypes);
|
||||
}
|
||||
}
|
||||
|
|
@ -489,7 +489,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
public static void NoFormatter(
|
||||
this ILogger logger,
|
||||
OutputFormatterWriteContext formatterContext)
|
||||
OutputFormatterCanWriteContext formatterContext)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
|
|
@ -500,7 +500,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
public static void FormatterSelected(
|
||||
this ILogger logger,
|
||||
IOutputFormatter outputFormatter,
|
||||
OutputFormatterWriteContext context)
|
||||
OutputFormatterCanWriteContext context)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
options.Value.OutputFormatters.Add(formatter.Object);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
options.Value.OutputFormatters.Add(formatter.Object);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
options.Value.OutputFormatters.Add(formatter.Object);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,459 @@
|
|||
// 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.Formatters;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class DefaultObjectResultExecutorTest
|
||||
{
|
||||
[Fact]
|
||||
public void SelectFormatter_UsesPassedInFormatters_IgnoresOptionsFormatters()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(), // This will be chosen based on the content type
|
||||
};
|
||||
var selector = CreateSelector(new IOutputFormatter[] { });
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
formatters,
|
||||
new MediaTypeCollection { "application/json" });
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(), // This will be chosen based on the content type
|
||||
};
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection { "application/json" });
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithOneProvidedContentType_NoFallback()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection { "application/json" });
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
// ObjectResult.ContentTypes, Accept header, expected content type
|
||||
public static TheoryData<MediaTypeCollection, string, string> ContentTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
var contentTypes = new MediaTypeCollection
|
||||
{
|
||||
"text/plain",
|
||||
"text/xml",
|
||||
"application/json",
|
||||
};
|
||||
|
||||
return new TheoryData<MediaTypeCollection, string, string>()
|
||||
{
|
||||
// Empty accept header, should select based on ObjectResult.ContentTypes.
|
||||
{ contentTypes, "", "application/json" },
|
||||
|
||||
// null accept header, should select based on ObjectResult.ContentTypes.
|
||||
{ contentTypes, null, "application/json" },
|
||||
|
||||
// The accept header does not match anything in ObjectResult.ContentTypes.
|
||||
// The first formatter that can write the result gets to choose the content type.
|
||||
{ contentTypes, "text/custom", "application/json" },
|
||||
|
||||
// Accept header matches ObjectResult.ContentTypes, but no formatter supports the accept header.
|
||||
// The first formatter that can write the result gets to choose the content type.
|
||||
{ contentTypes, "text/xml", "application/json" },
|
||||
|
||||
// Filters out Accept headers with 0 quality and selects the one with highest quality.
|
||||
{
|
||||
contentTypes,
|
||||
"text/plain;q=0.3, text/json;q=0, text/cusotm;q=0.0, application/json;q=0.4",
|
||||
"application/json"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ContentTypes))]
|
||||
public void SelectFormatter_WithMultipleProvidedContentTypes_DoesConneg(
|
||||
MediaTypeCollection contentTypes,
|
||||
string acceptHeader,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader;
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
contentTypes);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment(expectedContentType), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection());
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeader_UsesFallback()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection());
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("application/xml"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback()
|
||||
{
|
||||
// Arrange
|
||||
var options = new MvcOptions()
|
||||
{
|
||||
ReturnHttpNotAcceptable = true,
|
||||
OutputFormatters =
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
},
|
||||
};
|
||||
|
||||
var selector = CreateSelector(options);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection());
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom";
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
new MediaTypeCollection());
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, text/custom2";
|
||||
|
||||
var serverDefinedContentTypes = new MediaTypeCollection();
|
||||
serverDefinedContentTypes.Add("text/other");
|
||||
serverDefinedContentTypes.Add("text/custom2");
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
serverDefinedContentTypes);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("text/custom2"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue()
|
||||
{
|
||||
// Arrange
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var selector = CreateSelector(formatters);
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
var serverDefinedContentTypes = new MediaTypeCollection();
|
||||
serverDefinedContentTypes.Add("text/custom");
|
||||
|
||||
// Act
|
||||
var formatter = selector.SelectFormatter(
|
||||
context,
|
||||
Array.Empty<IOutputFormatter>(),
|
||||
serverDefinedContentTypes);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("text/custom"), context.ContentType);
|
||||
}
|
||||
|
||||
private static DefaultOutputFormatterSelector CreateSelector(IEnumerable<IOutputFormatter> formatters)
|
||||
{
|
||||
var options = new MvcOptions();
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
options.OutputFormatters.Add(formatter);
|
||||
}
|
||||
|
||||
return CreateSelector(options);
|
||||
}
|
||||
|
||||
private static DefaultOutputFormatterSelector CreateSelector(MvcOptions options)
|
||||
{
|
||||
return new DefaultOutputFormatterSelector(Options.Create(options), NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
private class CannotWriteFormatter : IOutputFormatter
|
||||
{
|
||||
public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual Task WriteAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestJsonOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestJsonOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestXmlOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestXmlOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestStringOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
public TestStringOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerContentTypeOnlyFormatter : OutputFormatter
|
||||
{
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
{
|
||||
// This test formatter matches if and only if the content type is specified
|
||||
// as "server defined". This lets tests identify what value the ObjectResultExecutor
|
||||
// passed for that flag.
|
||||
return context.ContentTypeIsServerDefined;
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,48 +21,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
public class ObjectResultExecutorTest
|
||||
{
|
||||
[Fact]
|
||||
public void SelectFormatter_WithNoProvidedContentType_DoesConneg()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(), // This will be chosen based on the accept header
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/json";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { "application/json" },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
MediaTypeAssert.Equal("application/json", context.ContentType);
|
||||
}
|
||||
|
||||
// For this test case probably the most common use case is when there is a format mapping based
|
||||
// content type selected but the developer had set the content type on the Response.ContentType
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ContentTypeProvidedFromResponseAndObjectResult_UsesResponseContentType()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateCustomObjectResultExecutor();
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var actionContext = new ActionContext() { HttpContext = httpContext };
|
||||
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
httpContext.Response.ContentType = "text/plain";
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
result.Formatters.Add(new TestXmlOutputFormatter());
|
||||
result.Formatters.Add(new TestJsonOutputFormatter());
|
||||
|
|
@ -72,50 +43,20 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<TestStringOutputFormatter>(executor.SelectedOutputFormatter);
|
||||
MediaTypeAssert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(), // This will be chosen based on the content type
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { "application/json" },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_IgnoresAcceptHeader()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateCustomObjectResultExecutor();
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var actionContext = new ActionContext() { HttpContext = httpContext };
|
||||
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
result.Formatters.Add(new TestXmlOutputFormatter());
|
||||
result.Formatters.Add(new TestJsonOutputFormatter()); // This will be chosen based on the content type
|
||||
|
|
@ -124,48 +65,20 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<TestJsonOutputFormatter>(executor.SelectedOutputFormatter);
|
||||
Assert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithOneProvidedContentType_NoFallback()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { "application/json" },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_NoFallback()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateCustomObjectResultExecutor();
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var actionContext = new ActionContext() { HttpContext = httpContext };
|
||||
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
result.Formatters.Add(new TestXmlOutputFormatter());
|
||||
|
||||
|
|
@ -173,268 +86,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Null(executor.SelectedOutputFormatter);
|
||||
}
|
||||
|
||||
// ObjectResult.ContentTypes, Accept header, expected content type
|
||||
public static TheoryData<MediaTypeCollection, string, string> ContentTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
var contentTypes = new MediaTypeCollection
|
||||
{
|
||||
"text/plain",
|
||||
"text/xml",
|
||||
"application/json",
|
||||
};
|
||||
|
||||
return new TheoryData<MediaTypeCollection, string, string>()
|
||||
{
|
||||
// Empty accept header, should select based on ObjectResult.ContentTypes.
|
||||
{ contentTypes, "", "application/json" },
|
||||
|
||||
// null accept header, should select based on ObjectResult.ContentTypes.
|
||||
{ contentTypes, null, "application/json" },
|
||||
|
||||
// The accept header does not match anything in ObjectResult.ContentTypes.
|
||||
// The first formatter that can write the result gets to choose the content type.
|
||||
{ contentTypes, "text/custom", "application/json" },
|
||||
|
||||
// Accept header matches ObjectResult.ContentTypes, but no formatter supports the accept header.
|
||||
// The first formatter that can write the result gets to choose the content type.
|
||||
{ contentTypes, "text/xml", "application/json" },
|
||||
|
||||
// Filters out Accept headers with 0 quality and selects the one with highest quality.
|
||||
{
|
||||
contentTypes,
|
||||
"text/plain;q=0.3, text/json;q=0, text/cusotm;q=0.0, application/json;q=0.4",
|
||||
"application/json"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ContentTypes))]
|
||||
public void SelectFormatter_WithMultipleProvidedContentTypes_DoesConneg(
|
||||
MediaTypeCollection contentTypes,
|
||||
string acceptHeader,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader;
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
contentTypes,
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment(expectedContentType), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection(),
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeader_UsesFallback()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("application/xml"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new MvcOptions());
|
||||
options.Value.ReturnHttpNotAcceptable = true;
|
||||
|
||||
var executor = CreateExecutor(options);
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, text/custom2";
|
||||
|
||||
var serverDefinedContentTypes = new MediaTypeCollection();
|
||||
serverDefinedContentTypes.Add("text/other");
|
||||
serverDefinedContentTypes.Add("text/custom2");
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
serverDefinedContentTypes,
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("text/custom2"), context.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new ServerContentTypeOnlyFormatter()
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
var serverDefinedContentTypes = new MediaTypeCollection();
|
||||
serverDefinedContentTypes.Add("text/custom");
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
serverDefinedContentTypes,
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("text/custom"), context.ContentType);
|
||||
Assert.Equal(406, httpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -637,20 +289,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return httpContext;
|
||||
}
|
||||
|
||||
private static TestObjectResultExecutor CreateExecutor(IOptions<MvcOptions> options = null)
|
||||
private static ObjectResultExecutor CreateExecutor(IOptions<MvcOptions> options = null)
|
||||
{
|
||||
return new TestObjectResultExecutor(
|
||||
options ?? Options.Create(new MvcOptions()),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
private static CustomObjectResultExecutor CreateCustomObjectResultExecutor()
|
||||
{
|
||||
return new CustomObjectResultExecutor(
|
||||
Options.Create(new MvcOptions()),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance);
|
||||
var selector = new DefaultOutputFormatterSelector(options ?? Options.Create<MvcOptions>(new MvcOptions()), NullLoggerFactory.Instance);
|
||||
return new ObjectResultExecutor(selector, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
private class CannotWriteFormatter : IOutputFormatter
|
||||
|
|
@ -713,47 +355,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
private class TestObjectResultExecutor : ObjectResultExecutor
|
||||
{
|
||||
public TestObjectResultExecutor(
|
||||
IOptions<MvcOptions> options,
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
: base(options, writerFactory, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
new public IOutputFormatter SelectFormatter(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
MediaTypeCollection contentTypes,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
return base.SelectFormatter(formatterContext, contentTypes, formatters);
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomObjectResultExecutor : ObjectResultExecutor
|
||||
{
|
||||
public CustomObjectResultExecutor(
|
||||
IOptions<MvcOptions> options,
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
: base(options, writerFactory, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public IOutputFormatter SelectedOutputFormatter { get; private set; }
|
||||
|
||||
protected override IOutputFormatter SelectFormatter(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
MediaTypeCollection contentTypes,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
SelectedOutputFormatter = base.SelectFormatter(formatterContext, contentTypes, formatters);
|
||||
return SelectedOutputFormatter;
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerContentTypeOnlyFormatter : OutputFormatter
|
||||
{
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
|
|
|
|||
|
|
@ -1596,14 +1596,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var options = new MvcOptions();
|
||||
var mvcOptionsAccessor = Options.Create(options);
|
||||
|
||||
var options = Options.Create(new MvcOptions());
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
services.AddSingleton<IOptions<MvcOptions>>(mvcOptionsAccessor);
|
||||
services.AddSingleton<IOptions<MvcOptions>>(options);
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
mvcOptionsAccessor,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
@ -1622,7 +1622,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
await c.HttpContext.Response.WriteAsync(c.Object.ToString());
|
||||
});
|
||||
|
||||
options.OutputFormatters.Add(formatter.Object);
|
||||
options.Value.OutputFormatters.Add(formatter.Object);
|
||||
|
||||
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
|
||||
if (diagnosticListener != null)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
Options.Create(new MvcOptions()),
|
||||
new DefaultOutputFormatterSelector(Options.Create(new MvcOptions()), NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ namespace System.Web.Http
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ namespace System.Web.Http
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ namespace System.Web.Http
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ namespace System.Web.Http
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
options,
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue