Move implementation of ObjectResult into a facade
This commit is contained in:
parent
6bd97c7c30
commit
06cc58663e
|
|
@ -39,10 +39,6 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
/// </summary>
|
||||
public MediaTypeHeaderValue SelectedContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status code that should be used for the response when successfully formatting.
|
||||
/// </summary>
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a flag to indicate that content-negotiation could not find a formatter based on the
|
||||
|
|
|
|||
|
|
@ -58,13 +58,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
public IDictionary<string, object> RouteValues { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnFormatting(ActionContext context)
|
||||
public override void OnFormatting(ActionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
base.OnFormatting(context);
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var urlHelper = UrlHelper ?? context.HttpContext.RequestServices.GetRequiredService<IUrlHelper>();
|
||||
|
||||
|
|
|
|||
|
|
@ -61,13 +61,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
public IDictionary<string, object> RouteValues { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnFormatting(ActionContext context)
|
||||
public override void OnFormatting(ActionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
base.OnFormatting(context);
|
||||
|
||||
var urlHelper = UrlHelper ?? context.HttpContext.RequestServices.GetRequiredService<IUrlHelper>();
|
||||
|
||||
var url = urlHelper.Link(RouteName, RouteValues);
|
||||
|
|
|
|||
|
|
@ -79,13 +79,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnFormatting(ActionContext context)
|
||||
public override void OnFormatting(ActionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
base.OnFormatting(context);
|
||||
|
||||
context.HttpContext.Response.Headers[HeaderNames.Location] = Location;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
|
||||
services.TryAddSingleton<IArraySegmentPool<byte>, DefaultArraySegmentPool<byte>>();
|
||||
services.TryAddSingleton<IArraySegmentPool<char>, DefaultArraySegmentPool<char>>();
|
||||
services.TryAddSingleton<ObjectResultExecutor>();
|
||||
}
|
||||
|
||||
private static void ConfigureDefaultServices(IServiceCollection services)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,12 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentLength = 0;
|
||||
response.StatusCode = context.StatusCode ?? StatusCodes.Status204NoContent;
|
||||
|
||||
if (response.StatusCode == StatusCodes.Status200OK)
|
||||
{
|
||||
response.StatusCode = StatusCodes.Status204NoContent;
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,432 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.AspNet.Mvc.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes an <see cref="ObjectResult"/> to write to the response.
|
||||
/// </summary>
|
||||
public class ObjectResultExecutor
|
||||
{
|
||||
private readonly IActionBindingContextAccessor _bindingContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectResultExecutor"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">An accessor to <see cref="MvcOptions"/>.</param>
|
||||
/// <param name="bindingContextAccessor">The <see cref="IActionBindingContextAccessor"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public ObjectResultExecutor(
|
||||
IOptions<MvcOptions> options,
|
||||
IActionBindingContextAccessor bindingContextAccessor,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (bindingContextAccessor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindingContextAccessor));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_bindingContextAccessor = bindingContextAccessor;
|
||||
|
||||
OptionsFormatters = options.Value.OutputFormatters;
|
||||
RespectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
|
||||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ActionBindingContext"/> for the current request.
|
||||
/// </summary>
|
||||
protected ActionBindingContext BindingContext => _bindingContextAccessor.ActionBindingContext;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <see cref="IOutputFormatter"/> instances from <see cref="MvcOptions"/>.
|
||||
/// </summary>
|
||||
protected IList<IOutputFormatter> OptionsFormatters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of <see cref="MvcOptions.RespectBrowserAcceptHeader"/>.
|
||||
/// </summary>
|
||||
protected bool RespectBrowserAcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the <see cref="ObjectResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ActionContext"/> for the current request.</param>
|
||||
/// <param name="result">The <see cref="ObjectResult"/>.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> which will complete once the <see cref="ObjectResult"/> is written to the response.
|
||||
/// </returns>
|
||||
public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
ValidateContentTypes(result.ContentTypes);
|
||||
|
||||
var formatters = result.Formatters;
|
||||
if (formatters == null || formatters.Count == 0)
|
||||
{
|
||||
formatters = GetDefaultFormatters();
|
||||
}
|
||||
|
||||
var formatterContext = new OutputFormatterContext()
|
||||
{
|
||||
DeclaredType = result.DeclaredType,
|
||||
HttpContext = context.HttpContext,
|
||||
Object = result.Value,
|
||||
};
|
||||
|
||||
var selectedFormatter = SelectFormatter(formatterContext, result.ContentTypes, formatters);
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// No formatter supports this.
|
||||
Logger.LogWarning("No output formatter was found to write the response.");
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
Logger.LogVerbose(
|
||||
"Selected output formatter '{OutputFormatter}' and content type " +
|
||||
"'{ContentType}' to write the response.",
|
||||
selectedFormatter.GetType().FullName,
|
||||
formatterContext.SelectedContentType);
|
||||
|
||||
result.OnFormatting(context);
|
||||
return selectedFormatter.WriteAsync(formatterContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterContext"/>.</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(
|
||||
OutputFormatterContext formatterContext,
|
||||
IList<MediaTypeHeaderValue> contentTypes,
|
||||
IEnumerable<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));
|
||||
}
|
||||
|
||||
// Check if any content-type was explicitly set (for example, via ProducesAttribute
|
||||
// or URL path extension mapping). If yes, then ignore content-negotiation and use this content-type.
|
||||
if (contentTypes.Count == 1)
|
||||
{
|
||||
Logger.LogVerbose(
|
||||
"Skipped content negotiation as content type '{ContentType}' is explicitly set for the response.",
|
||||
contentTypes[0]);
|
||||
|
||||
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, contentTypes);
|
||||
}
|
||||
|
||||
var sortedAcceptHeaderMediaTypes = GetSortedAcceptHeaderMediaTypes(formatterContext);
|
||||
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
if (contentTypes == null || contentTypes.Count == 0)
|
||||
{
|
||||
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
|
||||
// which can write the type. Let the formatter choose the Content-Type.
|
||||
if (!sortedAcceptHeaderMediaTypes.Any())
|
||||
{
|
||||
Logger.LogVerbose("No information found on request to perform content negotiation.");
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
|
||||
//
|
||||
// Content-Negotiation starts from this point on.
|
||||
//
|
||||
|
||||
// 1. Select based on sorted accept headers.
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
sortedAcceptHeaderMediaTypes);
|
||||
|
||||
// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
|
||||
// the type. Let the formatter choose the Content-Type.
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
Logger.LogVerbose("Could not find an output formatter based on content negotiation.");
|
||||
|
||||
// Set this flag to indicate that content-negotiation has failed to let formatters decide
|
||||
// if they want to write the response or not.
|
||||
formatterContext.FailedContentNegotiation = true;
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sortedAcceptHeaderMediaTypes.Any())
|
||||
{
|
||||
// Filter and remove accept headers which cannot support any of the user specified content types.
|
||||
// That is, confirm this result supports a more specific media type than requested e.g. OK if
|
||||
// "text/*" requested and result supports "text/plain".
|
||||
var filteredAndSortedAcceptHeaders = sortedAcceptHeaderMediaTypes
|
||||
.Where(acceptHeader => contentTypes.Any(contentType => contentType.IsSubsetOf(acceptHeader)));
|
||||
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
filteredAndSortedAcceptHeaders);
|
||||
}
|
||||
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// Either there were no acceptHeaders that were present OR
|
||||
// There were no accept headers which matched OR
|
||||
// There were acceptHeaders which matched but there was no formatter
|
||||
// which supported any of them.
|
||||
// In any of these cases, if the user has specified content types,
|
||||
// do a last effort to find a formatter which can write any of the user specified content type.
|
||||
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
|
||||
formatterContext,
|
||||
formatters,
|
||||
contentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
/// <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="OutputFormatterContext"/>.</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 SelectFormatterNotUsingAcceptHeaders(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters)
|
||||
{
|
||||
if (formatterContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatterContext));
|
||||
}
|
||||
|
||||
if (formatters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
if (formatter.CanWriteResult(formatterContext, contentType: null))
|
||||
{
|
||||
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="OutputFormatterContext"/>.</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(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters,
|
||||
IEnumerable<MediaTypeHeaderValue> 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));
|
||||
}
|
||||
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
foreach (var contentType in sortedAcceptHeaders)
|
||||
{
|
||||
// Loop through each of the formatters and see if any one will support this
|
||||
// mediaType Value.
|
||||
selectedFormatter = formatters.FirstOrDefault(
|
||||
formatter => formatter.CanWriteResult(formatterContext, contentType));
|
||||
if (selectedFormatter != null)
|
||||
{
|
||||
// we found our match.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
/// <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="OutputFormatterContext"/>.</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(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters,
|
||||
IEnumerable<MediaTypeHeaderValue> 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));
|
||||
}
|
||||
|
||||
var selectedFormatter = formatters.FirstOrDefault(
|
||||
formatter => acceptableContentTypes.Any(
|
||||
contentType => formatter.CanWriteResult(formatterContext, contentType)));
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
private IEnumerable<MediaTypeHeaderValue> GetSortedAcceptHeaderMediaTypes(
|
||||
OutputFormatterContext formatterContext)
|
||||
{
|
||||
var request = formatterContext.HttpContext.Request;
|
||||
var incomingAcceptHeaderMediaTypes = request.GetTypedHeaders().Accept ?? new MediaTypeHeaderValue[] { };
|
||||
|
||||
// By default we want to ignore considering accept headers for content negotiation when
|
||||
// they have a media type like */* in them. Browsers typically have these media types.
|
||||
// In these cases we would want the first formatter in the list of output formatters to
|
||||
// write the response. This default behavior can be changed through options, so checking here.
|
||||
var respectAcceptHeader = true;
|
||||
if (RespectBrowserAcceptHeader == false
|
||||
&& incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes))
|
||||
{
|
||||
respectAcceptHeader = false;
|
||||
}
|
||||
|
||||
var sortedAcceptHeaderMediaTypes = Enumerable.Empty<MediaTypeHeaderValue>();
|
||||
if (respectAcceptHeader)
|
||||
{
|
||||
sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes)
|
||||
.Where(header => header.Quality != HeaderQuality.NoMatch);
|
||||
}
|
||||
|
||||
return sortedAcceptHeaderMediaTypes;
|
||||
}
|
||||
|
||||
private void ValidateContentTypes(IList<MediaTypeHeaderValue> contentTypes)
|
||||
{
|
||||
var matchAllContentType = contentTypes?.FirstOrDefault(
|
||||
contentType => contentType.MatchesAllSubTypes || contentType.MatchesAllTypes);
|
||||
if (matchAllContentType != null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatObjectResult_MatchAllContentType(
|
||||
matchAllContentType,
|
||||
nameof(ObjectResult.ContentTypes)));
|
||||
}
|
||||
}
|
||||
|
||||
// This can't be cached, because
|
||||
private IList<IOutputFormatter> GetDefaultFormatters()
|
||||
{
|
||||
return BindingContext?.OutputFormatters ?? OptionsFormatters;
|
||||
}
|
||||
|
||||
private static IEnumerable<MediaTypeHeaderValue> SortMediaTypeHeaderValues(
|
||||
IEnumerable<MediaTypeHeaderValue> headerValues)
|
||||
{
|
||||
// Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons
|
||||
// are quite expensive so OrderBy() performs better.
|
||||
return headerValues.OrderByDescending(
|
||||
headerValue => headerValue,
|
||||
MediaTypeHeaderValueComparer.QualityComparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,16 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||
using Microsoft.AspNet.Mvc.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
@ -41,280 +35,24 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
public override Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ObjectResult>>();
|
||||
|
||||
// See if the list of content types added to this object result is valid.
|
||||
ThrowIfUnsupportedContentType();
|
||||
var formatters = GetDefaultFormatters(context);
|
||||
|
||||
var formatterContext = new OutputFormatterContext()
|
||||
{
|
||||
DeclaredType = DeclaredType,
|
||||
HttpContext = context.HttpContext,
|
||||
Object = Value,
|
||||
StatusCode = StatusCode
|
||||
};
|
||||
|
||||
var selectedFormatter = SelectFormatter(formatterContext, formatters);
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// No formatter supports this.
|
||||
logger.LogWarning("No output formatter was found to write the response.");
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
logger.LogVerbose(
|
||||
"Selected output formatter '{OutputFormatter}' and content type " +
|
||||
"'{ContentType}' to write the response.",
|
||||
selectedFormatter.GetType().FullName,
|
||||
formatterContext.SelectedContentType);
|
||||
|
||||
if (StatusCode.HasValue)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = StatusCode.Value;
|
||||
}
|
||||
|
||||
OnFormatting(context);
|
||||
return selectedFormatter.WriteAsync(formatterContext);
|
||||
}
|
||||
|
||||
public virtual IOutputFormatter SelectFormatter(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters)
|
||||
{
|
||||
var logger = formatterContext.HttpContext.RequestServices.GetRequiredService<ILogger<ObjectResult>>();
|
||||
|
||||
// Check if any content-type was explicitly set (for example, via ProducesAttribute
|
||||
// or URL path extension mapping). If yes, then ignore content-negotiation and use this content-type.
|
||||
if (ContentTypes.Count == 1)
|
||||
{
|
||||
logger.LogVerbose(
|
||||
"Skipped content negotiation as content type '{ContentType}' is explicitly set for the response.",
|
||||
ContentTypes[0]);
|
||||
|
||||
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, ContentTypes);
|
||||
}
|
||||
|
||||
var sortedAcceptHeaderMediaTypes = GetSortedAcceptHeaderMediaTypes(formatterContext);
|
||||
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
if (ContentTypes == null || ContentTypes.Count == 0)
|
||||
{
|
||||
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
|
||||
// which can write the type. Let the formatter choose the Content-Type.
|
||||
if (!sortedAcceptHeaderMediaTypes.Any())
|
||||
{
|
||||
logger.LogVerbose("No information found on request to perform content negotiation.");
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
|
||||
//
|
||||
// Content-Negotiation starts from this point on.
|
||||
//
|
||||
|
||||
// 1. Select based on sorted accept headers.
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
sortedAcceptHeaderMediaTypes);
|
||||
|
||||
// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
|
||||
// the type. Let the formatter choose the Content-Type.
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
logger.LogVerbose("Could not find an output formatter based on content negotiation.");
|
||||
|
||||
// Set this flag to indicate that content-negotiation has failed to let formatters decide
|
||||
// if they want to write the response or not.
|
||||
formatterContext.FailedContentNegotiation = true;
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sortedAcceptHeaderMediaTypes.Any())
|
||||
{
|
||||
// Filter and remove accept headers which cannot support any of the user specified content types.
|
||||
// That is, confirm this result supports a more specific media type than requested e.g. OK if
|
||||
// "text/*" requested and result supports "text/plain".
|
||||
var filteredAndSortedAcceptHeaders = sortedAcceptHeaderMediaTypes
|
||||
.Where(acceptHeader => ContentTypes.Any(contentType => contentType.IsSubsetOf(acceptHeader)));
|
||||
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
filteredAndSortedAcceptHeaders);
|
||||
}
|
||||
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
// Either there were no acceptHeaders that were present OR
|
||||
// There were no accept headers which matched OR
|
||||
// There were acceptHeaders which matched but there was no formatter
|
||||
// which supported any of them.
|
||||
// In any of these cases, if the user has specified content types,
|
||||
// do a last effort to find a formatter which can write any of the user specified content type.
|
||||
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
|
||||
formatterContext,
|
||||
formatters,
|
||||
ContentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
public virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters)
|
||||
{
|
||||
foreach (var formatter in formatters)
|
||||
{
|
||||
if (formatter.CanWriteResult(formatterContext, contentType: null))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters,
|
||||
IEnumerable<MediaTypeHeaderValue> sortedAcceptHeaders)
|
||||
{
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
foreach (var contentType in sortedAcceptHeaders)
|
||||
{
|
||||
// Loop through each of the formatters and see if any one will support this
|
||||
// mediaType Value.
|
||||
selectedFormatter = formatters.FirstOrDefault(
|
||||
formatter => formatter.CanWriteResult(formatterContext, contentType));
|
||||
if (selectedFormatter != null)
|
||||
{
|
||||
// we found our match.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
public virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
|
||||
OutputFormatterContext formatterContext,
|
||||
IEnumerable<IOutputFormatter> formatters,
|
||||
IEnumerable<MediaTypeHeaderValue> acceptableContentTypes)
|
||||
{
|
||||
var selectedFormatter = formatters.FirstOrDefault(
|
||||
formatter => acceptableContentTypes.Any(
|
||||
contentType => formatter.CanWriteResult(formatterContext, contentType)));
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
|
||||
private IEnumerable<MediaTypeHeaderValue> GetSortedAcceptHeaderMediaTypes(
|
||||
OutputFormatterContext formatterContext)
|
||||
{
|
||||
var request = formatterContext.HttpContext.Request;
|
||||
var incomingAcceptHeaderMediaTypes = request.GetTypedHeaders().Accept ?? new MediaTypeHeaderValue[] { };
|
||||
|
||||
// By default we want to ignore considering accept headers for content negotiation when
|
||||
// they have a media type like */* in them. Browsers typically have these media types.
|
||||
// In these cases we would want the first formatter in the list of output formatters to
|
||||
// write the response. This default behavior can be changed through options, so checking here.
|
||||
var options = formatterContext
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IOptions<MvcOptions>>()
|
||||
.Value;
|
||||
|
||||
var respectAcceptHeader = true;
|
||||
if (options.RespectBrowserAcceptHeader == false
|
||||
&& incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes))
|
||||
{
|
||||
respectAcceptHeader = false;
|
||||
}
|
||||
|
||||
var sortedAcceptHeaderMediaTypes = Enumerable.Empty<MediaTypeHeaderValue>();
|
||||
if (respectAcceptHeader)
|
||||
{
|
||||
sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes)
|
||||
.Where(header => header.Quality != HeaderQuality.NoMatch);
|
||||
}
|
||||
|
||||
return sortedAcceptHeaderMediaTypes;
|
||||
}
|
||||
|
||||
private void ThrowIfUnsupportedContentType()
|
||||
{
|
||||
var matchAllContentType = ContentTypes?.FirstOrDefault(
|
||||
contentType => contentType.MatchesAllSubTypes || contentType.MatchesAllTypes);
|
||||
if (matchAllContentType != null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatObjectResult_MatchAllContentType(matchAllContentType, nameof(ContentTypes)));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<MediaTypeHeaderValue> SortMediaTypeHeaderValues(
|
||||
IEnumerable<MediaTypeHeaderValue> headerValues)
|
||||
{
|
||||
// Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons
|
||||
// are quite expensive so OrderBy() performs better.
|
||||
return headerValues.OrderByDescending(
|
||||
headerValue => headerValue,
|
||||
MediaTypeHeaderValueComparer.QualityComparer);
|
||||
}
|
||||
|
||||
private IEnumerable<IOutputFormatter> GetDefaultFormatters(ActionContext context)
|
||||
{
|
||||
IEnumerable<IOutputFormatter> formatters = null;
|
||||
if (Formatters == null || Formatters.Count == 0)
|
||||
{
|
||||
var actionBindingContext = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IActionBindingContextAccessor>()
|
||||
.ActionBindingContext;
|
||||
|
||||
// In scenarios where there is a resource filter which directly short-circuits using an ObjectResult.
|
||||
// actionBindingContext is not setup yet and is null.
|
||||
if (actionBindingContext == null)
|
||||
{
|
||||
var options = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IOptions<MvcOptions>>()
|
||||
.Value;
|
||||
formatters = options.OutputFormatters;
|
||||
}
|
||||
else
|
||||
{
|
||||
formatters = actionBindingContext.OutputFormatters ?? new List<IOutputFormatter>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
formatters = Formatters;
|
||||
}
|
||||
|
||||
return formatters;
|
||||
var executor = context.HttpContext.RequestServices.GetRequiredService<ObjectResultExecutor>();
|
||||
return executor.ExecuteAsync(context, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is called before the formatter writes to the output stream.
|
||||
/// </summary>
|
||||
protected virtual void OnFormatting(ActionContext context)
|
||||
public virtual void OnFormatting(ActionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (StatusCode.HasValue)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = StatusCode.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,9 @@ using Microsoft.AspNet.Mvc.Infrastructure;
|
|||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -68,17 +70,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
"No route matches the supplied values.");
|
||||
}
|
||||
|
||||
private static HttpResponse GetMockedHttpResponseObject()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var httpResponse = new Mock<HttpResponse>();
|
||||
httpResponse.SetupProperty(o => o.StatusCode);
|
||||
httpResponse.Setup(o => o.Headers).Returns(
|
||||
new HeaderDictionary());
|
||||
httpResponse.SetupGet(o => o.Body).Returns(stream);
|
||||
return httpResponse.Object;
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(HttpContext httpContext)
|
||||
{
|
||||
var routeData = new RouteData();
|
||||
|
|
@ -88,34 +79,30 @@ namespace Microsoft.AspNet.Mvc
|
|||
routeData,
|
||||
new ActionDescriptor());
|
||||
}
|
||||
|
||||
private static HttpContext GetHttpContext()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.PathBase = new PathString("");
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
httpContext.RequestServices = services.Object;
|
||||
|
||||
var optionsAccessor = new TestOptionsManager<MvcOptions>();
|
||||
optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
services.Setup(p => p.GetService(typeof(IOptions<MvcOptions>)))
|
||||
.Returns(optionsAccessor);
|
||||
services.Setup(s => s.GetService(typeof(ILogger<ObjectResult>)))
|
||||
.Returns(new Mock<ILogger<ObjectResult>>().Object);
|
||||
|
||||
var actionBindingContext = new ActionBindingContext
|
||||
{
|
||||
OutputFormatters = optionsAccessor.Value.OutputFormatters
|
||||
};
|
||||
services.Setup(o => o.GetService(typeof(IActionBindingContextAccessor)))
|
||||
.Returns(new ActionBindingContextAccessor() { ActionBindingContext = actionBindingContext });
|
||||
|
||||
httpContext.RequestServices = CreateServices();
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddInstance(new ObjectResultExecutor(
|
||||
options,
|
||||
new ActionBindingContextAccessor(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static IUrlHelper GetMockUrlHelper(string returnValue)
|
||||
{
|
||||
var urlHelper = new Mock<IUrlHelper>();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ using Microsoft.AspNet.Mvc.Formatters;
|
|||
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -93,29 +95,26 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
private static HttpContext GetHttpContext()
|
||||
{
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var realContext = new DefaultHttpContext();
|
||||
var request = realContext.Request;
|
||||
request.PathBase = new PathString("");
|
||||
var response = realContext.Response;
|
||||
response.Body = new MemoryStream();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.PathBase = new PathString("");
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = CreateServices();
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
httpContext.Setup(o => o.Request)
|
||||
.Returns(request);
|
||||
var optionsAccessor = new TestOptionsManager<MvcOptions>();
|
||||
optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
|
||||
.Returns(optionsAccessor);
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(ILogger<ObjectResult>)))
|
||||
.Returns(new Mock<ILogger<ObjectResult>>().Object);
|
||||
httpContext.Setup(o => o.Response)
|
||||
.Returns(response);
|
||||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(IActionBindingContextAccessor)))
|
||||
.Returns(new ActionBindingContextAccessor());
|
||||
var services = new ServiceCollection();
|
||||
services.AddInstance(new ObjectResultExecutor(
|
||||
options,
|
||||
new ActionBindingContextAccessor(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
return httpContext.Object;
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static IUrlHelper GetMockUrlHelper(string returnValue)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
|
@ -10,7 +11,9 @@ using Microsoft.AspNet.Mvc.Formatters;
|
|||
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -79,36 +82,26 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
private static HttpContext GetHttpContext()
|
||||
{
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var realContext = new DefaultHttpContext();
|
||||
var request = realContext.Request;
|
||||
request.PathBase = new PathString("");
|
||||
var response = realContext.Response;
|
||||
response.Body = new MemoryStream();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.PathBase = new PathString("");
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = CreateServices();
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
httpContext.Setup(o => o.Request)
|
||||
.Returns(request);
|
||||
httpContext.Setup(o => o.Response)
|
||||
.Returns(response);
|
||||
var optionsAccessor = new TestOptionsManager<MvcOptions>();
|
||||
optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
httpContext
|
||||
.Setup(p => p.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
|
||||
.Returns(optionsAccessor);
|
||||
httpContext
|
||||
.Setup(p => p.RequestServices.GetService(typeof(ILogger<ObjectResult>)))
|
||||
.Returns(new Mock<ILogger<ObjectResult>>().Object);
|
||||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
|
||||
var actionBindingContext = new ActionBindingContext()
|
||||
{
|
||||
OutputFormatters = optionsAccessor.Value.OutputFormatters
|
||||
};
|
||||
httpContext
|
||||
.Setup(o => o.RequestServices.GetService(typeof(IActionBindingContextAccessor)))
|
||||
.Returns(new ActionBindingContextAccessor() { ActionBindingContext = actionBindingContext });
|
||||
var services = new ServiceCollection();
|
||||
services.AddInstance(new ObjectResultExecutor(
|
||||
options,
|
||||
new ActionBindingContextAccessor(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
return httpContext.Object;
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Formatters
|
||||
{
|
||||
public class HttpNotAcceptableOutputFormatterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(null)]
|
||||
public void CanWriteResult_ReturnsFalse_WhenConnegHasntFailed(bool? connegFailedValue)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
FailedContentNegotiation = connegFailedValue,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = formatter.CanWriteResult(context, contentType: null);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteResult_ReturnsTrue_WhenConnegHasFailed()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
FailedContentNegotiation = true,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = formatter.CanWriteResult(context, contentType: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_Sets406NotAcceptable()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await formatter.WriteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status406NotAcceptable, context.HttpContext.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
public void CanWriteResult_ByDefault_ReturnsTrue_IfTheValueIsNull(
|
||||
object value,
|
||||
bool declaredTypeAsString,
|
||||
bool expectedCanwriteResult,
|
||||
bool expected,
|
||||
bool useNonNullContentType)
|
||||
{
|
||||
// Arrange
|
||||
|
|
@ -47,14 +47,14 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
DeclaredType = typeToUse,
|
||||
HttpContext = null,
|
||||
};
|
||||
var contetType = useNonNullContentType ? MediaTypeHeaderValue.Parse("text/plain") : null;
|
||||
var contentType = useNonNullContentType ? MediaTypeHeaderValue.Parse("text/plain") : null;
|
||||
var formatter = new HttpNoContentOutputFormatter();
|
||||
|
||||
// Act
|
||||
var actualCanWriteResult = formatter.CanWriteResult(formatterContext, contetType);
|
||||
var result = formatter.CanWriteResult(formatterContext, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCanwriteResult, actualCanWriteResult);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -69,24 +69,24 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
DeclaredType = declaredType,
|
||||
HttpContext = null,
|
||||
};
|
||||
var contetType = MediaTypeHeaderValue.Parse("text/plain");
|
||||
var contentType = MediaTypeHeaderValue.Parse("text/plain");
|
||||
var formatter = new HttpNoContentOutputFormatter();
|
||||
|
||||
// Act
|
||||
var actualCanWriteResult = formatter.CanWriteResult(formatterContext, contetType);
|
||||
var result = formatter.CanWriteResult(formatterContext, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.True(actualCanWriteResult);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, true, true)]
|
||||
[InlineData(null, false, false)]
|
||||
[InlineData("some value", true, false)]
|
||||
public void
|
||||
CanWriteResult_ReturnsTrue_IfReturnValueIsNullAndTreatNullValueAsNoContentIsNotSet(string value,
|
||||
bool treatNullValueAsNoContent,
|
||||
bool expectedCanwriteResult)
|
||||
public void CanWriteResult_ReturnsTrue_IfReturnValueIsNullAndTreatNullValueAsNoContentIsNotSet(
|
||||
string value,
|
||||
bool treatNullValueAsNoContent,
|
||||
bool expected)
|
||||
{
|
||||
// Arrange
|
||||
var formatterContext = new OutputFormatterContext()
|
||||
|
|
@ -96,28 +96,28 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
HttpContext = null,
|
||||
};
|
||||
|
||||
var contetType = MediaTypeHeaderValue.Parse("text/plain");
|
||||
var contentType = MediaTypeHeaderValue.Parse("text/plain");
|
||||
var formatter = new HttpNoContentOutputFormatter()
|
||||
{
|
||||
TreatNullValueAsNoContent = treatNullValueAsNoContent
|
||||
};
|
||||
|
||||
// Act
|
||||
var actualCanWriteResult = formatter.CanWriteResult(formatterContext, contetType);
|
||||
var result = formatter.CanWriteResult(formatterContext, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCanwriteResult, actualCanWriteResult);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_WritesTheStatusCode204()
|
||||
{
|
||||
// Arrange
|
||||
var defaultHttpContext = new DefaultHttpContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
var formatterContext = new OutputFormatterContext()
|
||||
{
|
||||
Object = null,
|
||||
HttpContext = defaultHttpContext,
|
||||
HttpContext = httpContext,
|
||||
};
|
||||
|
||||
var formatter = new HttpNoContentOutputFormatter();
|
||||
|
|
@ -126,19 +126,19 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
await formatter.WriteAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status204NoContent, defaultHttpContext.Response.StatusCode);
|
||||
Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_ContextStatusCodeSet_WritesSameStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
var defaultHttpContext = new DefaultHttpContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.StatusCode = StatusCodes.Status201Created;
|
||||
|
||||
var formatterContext = new OutputFormatterContext()
|
||||
{
|
||||
Object = null,
|
||||
HttpContext = defaultHttpContext,
|
||||
StatusCode = StatusCodes.Status201Created
|
||||
HttpContext = httpContext,
|
||||
};
|
||||
|
||||
var formatter = new HttpNoContentOutputFormatter();
|
||||
|
|
@ -147,7 +147,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
|
|||
await formatter.WriteAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status201Created, defaultHttpContext.Response.StatusCode);
|
||||
Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Moq;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
@ -46,80 +41,43 @@ namespace Microsoft.AspNet.Mvc
|
|||
public async Task HttpNotFoundObjectResult_ExecuteSuccessful()
|
||||
{
|
||||
// Arrange
|
||||
var input = "Test Content";
|
||||
var stream = new MemoryStream();
|
||||
|
||||
var httpResponse = new Mock<HttpResponse>();
|
||||
var tempContentType = string.Empty;
|
||||
httpResponse.SetupProperty(o => o.ContentType);
|
||||
httpResponse.SetupGet(r => r.Body).Returns(stream);
|
||||
|
||||
var actionContext = CreateMockActionContext(httpResponse.Object);
|
||||
var notFound = new HttpNotFoundObjectResult(input);
|
||||
|
||||
// Act
|
||||
await notFound.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
httpResponse.VerifySet(r => r.StatusCode = StatusCodes.Status404NotFound);
|
||||
Assert.Equal(input.Length, httpResponse.Object.Body.Length);
|
||||
}
|
||||
|
||||
private static ActionContext CreateMockActionContext(
|
||||
HttpResponse response = null,
|
||||
string requestAcceptHeader = "application/*",
|
||||
string requestContentType = "application/json",
|
||||
string requestAcceptCharsetHeader = "",
|
||||
bool respectBrowserAcceptHeader = false)
|
||||
{
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
if (response != null)
|
||||
var httpContext = GetHttpContext();
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
httpContext.Setup(o => o.Response).Returns(response);
|
||||
}
|
||||
|
||||
var content = "{name: 'Person Name', Age: 'not-an-age'}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var request = new DefaultHttpContext().Request;
|
||||
request.Headers["Accept-Charset"] = requestAcceptCharsetHeader;
|
||||
request.Headers["Accept"] = requestAcceptHeader;
|
||||
request.ContentType = requestContentType;
|
||||
request.Body = new MemoryStream(contentBytes);
|
||||
|
||||
httpContext.Setup(o => o.Request).Returns(request);
|
||||
httpContext.Setup(o => o.RequestServices).Returns(GetServiceProvider());
|
||||
var optionsAccessor = new TestOptionsManager<MvcOptions>();
|
||||
optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
optionsAccessor.Value.RespectBrowserAcceptHeader = respectBrowserAcceptHeader;
|
||||
var actionBindingContextAccessor = new ActionBindingContextAccessor()
|
||||
{
|
||||
ActionBindingContext = new ActionBindingContext()
|
||||
{
|
||||
OutputFormatters = optionsAccessor.Value.OutputFormatters
|
||||
}
|
||||
HttpContext = httpContext,
|
||||
};
|
||||
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(IActionBindingContextAccessor)))
|
||||
.Returns(actionBindingContextAccessor);
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
|
||||
.Returns(optionsAccessor);
|
||||
httpContext.Setup(o => o.RequestServices.GetService(typeof(ILogger<ObjectResult>)))
|
||||
.Returns(new Mock<ILogger<ObjectResult>>().Object);
|
||||
var result = new HttpNotFoundObjectResult("Test Content");
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
private static IServiceProvider GetServiceProvider()
|
||||
private static HttpContext GetHttpContext()
|
||||
{
|
||||
var options = new MvcOptions();
|
||||
var optionsAccessor = new Mock<IOptions<MvcOptions>>();
|
||||
optionsAccessor.SetupGet(o => o.Value).Returns(options);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.PathBase = new PathString("");
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = CreateServices();
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddInstance(optionsAccessor.Object);
|
||||
return serviceCollection.BuildServiceProvider();
|
||||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddInstance(new ObjectResultExecutor(
|
||||
options,
|
||||
new ActionBindingContextAccessor(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,26 +69,17 @@ namespace Microsoft.AspNet.Mvc
|
|||
Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
private IServiceProvider CreateServices()
|
||||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.Add(new ServiceDescriptor(
|
||||
typeof(ILogger<ObjectResult>),
|
||||
new Logger<ObjectResult>(NullLoggerFactory.Instance)));
|
||||
|
||||
var optionsAccessor = new TestOptionsManager<MvcOptions>();
|
||||
optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
|
||||
services.Add(new ServiceDescriptor(typeof(IOptions<MvcOptions>), optionsAccessor));
|
||||
|
||||
var bindingContext = new ActionBindingContext
|
||||
{
|
||||
OutputFormatters = optionsAccessor.Value.OutputFormatters,
|
||||
};
|
||||
var bindingContextAccessor = new ActionBindingContextAccessor
|
||||
{
|
||||
ActionBindingContext = bindingContext,
|
||||
};
|
||||
services.Add(new ServiceDescriptor(typeof(IActionBindingContextAccessor), bindingContextAccessor));
|
||||
services.AddInstance(new ObjectResultExecutor(
|
||||
options,
|
||||
new ActionBindingContextAccessor(),
|
||||
NullLoggerFactory.Instance));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,502 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.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 OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/json";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new[] { new MediaTypeHeaderValue("application/json") },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new MediaTypeHeaderValue("application/json"), context.SelectedContentType);
|
||||
}
|
||||
|
||||
[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 OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new[] { new MediaTypeHeaderValue("application/json") },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new MediaTypeHeaderValue("application/json"), context.SelectedContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithOneProvidedContentType_NoFallback()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new[] { new MediaTypeHeaderValue("application/json") },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
// ObjectResult.ContentTypes, Accept header, expected content type
|
||||
public static TheoryData<string[], string, string> ContentTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
var contentTypes = new string[]
|
||||
{
|
||||
"text/plain",
|
||||
"text/xml",
|
||||
"application/json",
|
||||
};
|
||||
|
||||
return new TheoryData<string[], 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(
|
||||
IEnumerable<string> contentTypes,
|
||||
string acceptHeader,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader;
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)).ToList(),
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new MediaTypeHeaderValue(expectedContentType), context.SelectedContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormattterThatCanWrite()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
new TestXmlOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new List<MediaTypeHeaderValue>(),
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new MediaTypeHeaderValue("application/json"), context.SelectedContentType);
|
||||
Assert.Null(context.FailedContentNegotiation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeader_ConnegFails()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeHeaderValue[] { },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new MediaTypeHeaderValue("application/xml"), context.SelectedContentType);
|
||||
Assert.True(context.FailedContentNegotiation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_NoFormatterFound_Returns406()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
|
||||
// This formatter won't write anything
|
||||
result.Formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new CannotWriteFormatter(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status406NotAcceptable, actionContext.HttpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_FallsBackOnFormattersInBindingContext()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = new ActionBindingContext()
|
||||
{
|
||||
OutputFormatters = new List<IOutputFormatter>()
|
||||
{
|
||||
new TestJsonOutputFormatter(),
|
||||
}
|
||||
};
|
||||
|
||||
var executor = CreateExecutor(bindingContext: bindingContext);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
|
||||
var result = new ObjectResult("someValue");
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"application/json; charset=utf-8",
|
||||
actionContext.HttpContext.Response.Headers[HeaderNames.ContentType]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_FallsBackOnFormattersInOptions()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.OutputFormatters.Add(new TestJsonOutputFormatter());
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
|
||||
var result = new ObjectResult("someValue");
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"application/json; charset=utf-8",
|
||||
actionContext.HttpContext.Response.Headers[HeaderNames.ContentType]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[] { "application/*" }, "application/*")]
|
||||
[InlineData(new[] { "application/xml", "application/*", "application/json" }, "application/*")]
|
||||
[InlineData(new[] { "application/*", "application/json" }, "application/*")]
|
||||
[InlineData(new[] { "*/*" }, "*/*")]
|
||||
[InlineData(new[] { "application/xml", "*/*", "application/json" }, "*/*")]
|
||||
[InlineData(new[] { "*/*", "application/json" }, "*/*")]
|
||||
public async Task ExecuteAsync_MatchAllContentType_Throws(string[] contentTypes, string invalidContentType)
|
||||
{
|
||||
// Arrange
|
||||
var result = new ObjectResult("input");
|
||||
result.ContentTypes = contentTypes
|
||||
.Select(contentType => MediaTypeHeaderValue.Parse(contentType))
|
||||
.ToList();
|
||||
|
||||
var executor = CreateExecutor();
|
||||
|
||||
var actionContext = new ActionContext();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => executor.ExecuteAsync(actionContext, result));
|
||||
|
||||
var expectedMessage = string.Format("The content-type '{0}' added in the 'ContentTypes' property is " +
|
||||
"invalid. Media types which match all types or match all subtypes are not supported.",
|
||||
invalidContentType);
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Chrome & Opera
|
||||
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "application/json; charset=utf-8")]
|
||||
// IE
|
||||
[InlineData("text/html, application/xhtml+xml, */*", "application/json; charset=utf-8")]
|
||||
// Firefox & Safari
|
||||
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "application/json; charset=utf-8")]
|
||||
// Misc
|
||||
[InlineData("*/*", @"application/json; charset=utf-8")]
|
||||
[InlineData("text/html,*/*;q=0.8,application/xml;q=0.9", "application/json; charset=utf-8")]
|
||||
public async Task ExecuteAsync_SelectDefaultFormatter_OnAllMediaRangeAcceptHeaderMediaType(
|
||||
string acceptHeader,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.RespectBrowserAcceptHeader = false;
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
result.Formatters.Add(new TestJsonOutputFormatter());
|
||||
result.Formatters.Add(new TestXmlOutputFormatter());
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
actionContext.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader;
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContentType, actionContext.HttpContext.Response.Headers[HeaderNames.ContentType]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Chrome & Opera
|
||||
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "application/xml; charset=utf-8")]
|
||||
// IE
|
||||
[InlineData("text/html, application/xhtml+xml, */*", "application/json; charset=utf-8")]
|
||||
// Firefox & Safari
|
||||
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "application/xml; charset=utf-8")]
|
||||
// Misc
|
||||
[InlineData("*/*", @"application/json; charset=utf-8")]
|
||||
[InlineData("text/html,*/*;q=0.8,application/xml;q=0.9", "application/xml; charset=utf-8")]
|
||||
public async Task ObjectResult_PerformsContentNegotiation_OnAllMediaRangeAcceptHeaderMediaType(
|
||||
string acceptHeader,
|
||||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.RespectBrowserAcceptHeader = true;
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
var result = new ObjectResult("input");
|
||||
result.Formatters.Add(new TestJsonOutputFormatter());
|
||||
result.Formatters.Add(new TestXmlOutputFormatter());
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext(),
|
||||
};
|
||||
actionContext.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader;
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContentType, actionContext.HttpContext.Response.Headers[HeaderNames.ContentType]);
|
||||
}
|
||||
|
||||
private static TestObjectResultExecutor CreateExecutor(
|
||||
IOptions<MvcOptions> options = null,
|
||||
ActionBindingContext bindingContext = null)
|
||||
{
|
||||
var bindingContextAccessor = new ActionBindingContextAccessor();
|
||||
if (bindingContext != null)
|
||||
{
|
||||
bindingContextAccessor.ActionBindingContext = bindingContext;
|
||||
}
|
||||
|
||||
return new TestObjectResultExecutor(
|
||||
options ?? new TestOptionsManager<MvcOptions>(),
|
||||
bindingContextAccessor,
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
private class CannotWriteFormatter : IOutputFormatter
|
||||
{
|
||||
public virtual bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual Task WriteAsync(OutputFormatterContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestJsonOutputFormatter : OutputFormatter
|
||||
{
|
||||
public TestJsonOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestXmlOutputFormatter : OutputFormatter
|
||||
{
|
||||
public TestXmlOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
|
||||
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestObjectResultExecutor : ObjectResultExecutor
|
||||
{
|
||||
public TestObjectResultExecutor(
|
||||
IOptions<MvcOptions> options,
|
||||
IActionBindingContextAccessor bindingContextAccessor,
|
||||
ILoggerFactory loggerFactory)
|
||||
: base(options, bindingContextAccessor, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public new IOutputFormatter SelectFormatter(
|
||||
OutputFormatterContext formatterContext,
|
||||
IList<MediaTypeHeaderValue> contentTypes,
|
||||
IEnumerable<IOutputFormatter> formatters)
|
||||
{
|
||||
return base.SelectFormatter(formatterContext, contentTypes, formatters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue