Move implementation of ObjectResult into a facade

This commit is contained in:
Ryan Nowak 2015-10-14 21:36:23 -07:00
parent 6bd97c7c30
commit 06cc58663e
17 changed files with 1180 additions and 1394 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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