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:
Ryan Nowak 2017-10-31 16:32:37 -07:00 committed by GitHub
parent 41efa409a4
commit 861d78fb78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 868 additions and 801 deletions

View File

@ -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>();

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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))
{

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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);
}
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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);

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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));