diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterContext.cs
index ecf5d302ae..e5953e866d 100644
--- a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterContext.cs
+++ b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterContext.cs
@@ -39,10 +39,6 @@ namespace Microsoft.AspNet.Mvc.Formatters
///
public MediaTypeHeaderValue SelectedContentType { get; set; }
- ///
- /// Gets the status code that should be used for the response when successfully formatting.
- ///
- public int? StatusCode { get; set; }
///
/// Gets or sets a flag to indicate that content-negotiation could not find a formatter based on the
diff --git a/src/Microsoft.AspNet.Mvc.Core/CreatedAtActionResult.cs b/src/Microsoft.AspNet.Mvc.Core/CreatedAtActionResult.cs
index 0df97907ec..c7f1233869 100644
--- a/src/Microsoft.AspNet.Mvc.Core/CreatedAtActionResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/CreatedAtActionResult.cs
@@ -58,13 +58,15 @@ namespace Microsoft.AspNet.Mvc
public IDictionary RouteValues { get; set; }
///
- 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();
diff --git a/src/Microsoft.AspNet.Mvc.Core/CreatedAtRouteResult.cs b/src/Microsoft.AspNet.Mvc.Core/CreatedAtRouteResult.cs
index 2abc88753d..80143dc36e 100644
--- a/src/Microsoft.AspNet.Mvc.Core/CreatedAtRouteResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/CreatedAtRouteResult.cs
@@ -61,13 +61,15 @@ namespace Microsoft.AspNet.Mvc
public IDictionary RouteValues { get; set; }
///
- 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();
var url = urlHelper.Link(RouteName, RouteValues);
diff --git a/src/Microsoft.AspNet.Mvc.Core/CreatedResult.cs b/src/Microsoft.AspNet.Mvc.Core/CreatedResult.cs
index aa54c6bdcb..20faafc56f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/CreatedResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/CreatedResult.cs
@@ -79,13 +79,15 @@ namespace Microsoft.AspNet.Mvc
}
///
- 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;
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index 956124a879..4b8dc5195c 100644
--- a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -141,6 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton();
services.TryAddSingleton, DefaultArraySegmentPool>();
services.TryAddSingleton, DefaultArraySegmentPool>();
+ services.TryAddSingleton();
}
private static void ConfigureDefaultServices(IServiceCollection services)
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
index b8fc01fc56..aff430578c 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
@@ -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);
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs
new file mode 100644
index 0000000000..bc2d7886b6
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs
@@ -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
+{
+ ///
+ /// Executes an to write to the response.
+ ///
+ public class ObjectResultExecutor
+ {
+ private readonly IActionBindingContextAccessor _bindingContextAccessor;
+
+ ///
+ /// Creates a new .
+ ///
+ /// An accessor to .
+ /// The .
+ /// The .
+ public ObjectResultExecutor(
+ IOptions 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();
+ }
+
+ ///
+ /// Gets the for the current request.
+ ///
+ protected ActionBindingContext BindingContext => _bindingContextAccessor.ActionBindingContext;
+
+ ///
+ /// Gets the .
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// Gets the list of instances from .
+ ///
+ protected IList OptionsFormatters { get; }
+
+ ///
+ /// Gets the value of .
+ ///
+ protected bool RespectBrowserAcceptHeader { get; }
+
+ ///
+ /// Executes the .
+ ///
+ /// The for the current request.
+ /// The .
+ ///
+ /// A which will complete once the is written to the response.
+ ///
+ 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);
+ }
+
+ ///
+ /// Selects the to write the response.
+ ///
+ /// The .
+ ///
+ /// The list of content types provided by .
+ ///
+ ///
+ /// The list of instances to consider.
+ ///
+ ///
+ /// The selected or null if no formatter can write the response.
+ ///
+ protected virtual IOutputFormatter SelectFormatter(
+ OutputFormatterContext formatterContext,
+ IList contentTypes,
+ IEnumerable 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;
+ }
+
+ ///
+ /// Selects the to write the response. The first formatter which
+ /// can write the response should be chosen without any consideration for content type.
+ ///
+ /// The .
+ ///
+ /// The list of instances to consider.
+ ///
+ ///
+ /// The selected or null if no formatter can write the response.
+ ///
+ protected virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
+ OutputFormatterContext formatterContext,
+ IEnumerable 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;
+ }
+
+ ///
+ /// Selects the to write the response based on the content type values
+ /// present in .
+ ///
+ /// The .
+ ///
+ /// The list of instances to consider.
+ ///
+ ///
+ /// The ordered content types from the Accept header, sorted by descending q-value.
+ ///
+ ///
+ /// The selected or null if no formatter can write the response.
+ ///
+ protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
+ OutputFormatterContext formatterContext,
+ IEnumerable formatters,
+ IEnumerable 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;
+ }
+
+ ///
+ /// Selects the to write the response based on the content type values
+ /// present in .
+ ///
+ /// The .
+ ///
+ /// The list of instances to consider.
+ ///
+ ///
+ /// The ordered content types from in descending priority order.
+ ///
+ ///
+ /// The selected or null if no formatter can write the response.
+ ///
+ protected virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
+ OutputFormatterContext formatterContext,
+ IEnumerable formatters,
+ IEnumerable 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 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();
+ if (respectAcceptHeader)
+ {
+ sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes)
+ .Where(header => header.Quality != HeaderQuality.NoMatch);
+ }
+
+ return sortedAcceptHeaderMediaTypes;
+ }
+
+ private void ValidateContentTypes(IList 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 GetDefaultFormatters()
+ {
+ return BindingContext?.OutputFormatters ?? OptionsFormatters;
+ }
+
+ private static IEnumerable SortMediaTypeHeaderValues(
+ IEnumerable 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);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ObjectResult.cs
index 2abcbd08f7..3fe3c3e7b8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ObjectResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ObjectResult.cs
@@ -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>();
-
- // 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 formatters)
- {
- var logger = formatterContext.HttpContext.RequestServices.GetRequiredService>();
-
- // 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 formatters)
- {
- foreach (var formatter in formatters)
- {
- if (formatter.CanWriteResult(formatterContext, contentType: null))
- {
- return formatter;
- }
- }
-
- return null;
- }
-
- public virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
- OutputFormatterContext formatterContext,
- IEnumerable formatters,
- IEnumerable 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 formatters,
- IEnumerable acceptableContentTypes)
- {
- var selectedFormatter = formatters.FirstOrDefault(
- formatter => acceptableContentTypes.Any(
- contentType => formatter.CanWriteResult(formatterContext, contentType)));
-
- return selectedFormatter;
- }
-
- private IEnumerable 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>()
- .Value;
-
- var respectAcceptHeader = true;
- if (options.RespectBrowserAcceptHeader == false
- && incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes))
- {
- respectAcceptHeader = false;
- }
-
- var sortedAcceptHeaderMediaTypes = Enumerable.Empty();
- 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 SortMediaTypeHeaderValues(
- IEnumerable 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 GetDefaultFormatters(ActionContext context)
- {
- IEnumerable formatters = null;
- if (Formatters == null || Formatters.Count == 0)
- {
- var actionBindingContext = context
- .HttpContext
- .RequestServices
- .GetRequiredService()
- .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>()
- .Value;
- formatters = options.OutputFormatters;
- }
- else
- {
- formatters = actionBindingContext.OutputFormatters ?? new List();
- }
- }
- else
- {
- formatters = Formatters;
- }
-
- return formatters;
+ var executor = context.HttpContext.RequestServices.GetRequiredService();
+ return executor.ExecuteAsync(context, this);
}
///
/// This method is called before the formatter writes to the output stream.
///
- 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;
+ }
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtActionResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtActionResultTests.cs
index dd8c9ee897..270a3ca166 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtActionResultTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtActionResultTests.cs
@@ -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.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();
- httpContext.RequestServices = services.Object;
-
- var optionsAccessor = new TestOptionsManager();
- optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
- optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
- services.Setup(p => p.GetService(typeof(IOptions)))
- .Returns(optionsAccessor);
- services.Setup(s => s.GetService(typeof(ILogger)))
- .Returns(new Mock>().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();
+ 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();
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtRouteResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtRouteResultTests.cs
index cd64334085..6c51371fa1 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtRouteResultTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedAtRouteResultTests.cs
@@ -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();
- 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();
- optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
- optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
- httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions)))
- .Returns(optionsAccessor);
- httpContext.Setup(o => o.RequestServices.GetService(typeof(ILogger)))
- .Returns(new Mock>().Object);
- httpContext.Setup(o => o.Response)
- .Returns(response);
+ private static IServiceProvider CreateServices()
+ {
+ var options = new TestOptionsManager();
+ 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)
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedResultTests.cs
index b3a181f041..fdd3dfbb93 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/CreatedResultTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/CreatedResultTests.cs
@@ -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();
- 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();
- optionsAccessor.Value.OutputFormatters.Add(new StringOutputFormatter());
- optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
- httpContext
- .Setup(p => p.RequestServices.GetService(typeof(IOptions)))
- .Returns(optionsAccessor);
- httpContext
- .Setup(p => p.RequestServices.GetService(typeof(ILogger)))
- .Returns(new Mock>().Object);
+ private static IServiceProvider CreateServices()
+ {
+ var options = new TestOptionsManager();
+ 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();
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/HttpNotAcceptableOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/HttpNotAcceptableOutputFormatterTest.cs
new file mode 100644
index 0000000000..9260fec2c8
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/HttpNotAcceptableOutputFormatterTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs
index f4146eeac7..19445584ff 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs
@@ -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);
}
}
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs
index 6ddfc076fe..6ffa9a84bb 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs
@@ -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();
- 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();
- 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();
- 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)))
- .Returns(optionsAccessor);
- httpContext.Setup(o => o.RequestServices.GetService(typeof(ILogger)))
- .Returns(new Mock>().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>();
- 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();
+ 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();
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/HttpOkObjectResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/HttpOkObjectResultTest.cs
index a965676d88..ef378c1990 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/HttpOkObjectResultTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/HttpOkObjectResultTest.cs
@@ -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();
+ options.Value.OutputFormatters.Add(new StringOutputFormatter());
+ options.Value.OutputFormatters.Add(new JsonOutputFormatter());
+
var services = new ServiceCollection();
- services.Add(new ServiceDescriptor(
- typeof(ILogger),
- new Logger(NullLoggerFactory.Instance)));
-
- var optionsAccessor = new TestOptionsManager();
- optionsAccessor.Value.OutputFormatters.Add(new JsonOutputFormatter());
- services.Add(new ServiceDescriptor(typeof(IOptions), 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();
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs
new file mode 100644
index 0000000000..3736df1d27
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs
@@ -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
+ {
+ 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
+ {
+ 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
+ {
+ 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 ContentTypes
+ {
+ get
+ {
+ var contentTypes = new string[]
+ {
+ "text/plain",
+ "text/xml",
+ "application/json",
+ };
+
+ return new TheoryData()
+ {
+ // 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 contentTypes,
+ string acceptHeader,
+ string expectedContentType)
+ {
+ // Arrange
+ var executor = CreateExecutor();
+
+ var formatters = new List
+ {
+ 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
+ {
+ new CannotWriteFormatter(),
+ new TestJsonOutputFormatter(),
+ new TestXmlOutputFormatter(),
+ };
+
+ var context = new OutputFormatterContext()
+ {
+ HttpContext = new DefaultHttpContext(),
+ };
+
+ // Act
+ var formatter = executor.SelectFormatter(
+ context,
+ new List(),
+ 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
+ {
+ 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
+ {
+ 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()
+ {
+ 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();
+ 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(
+ () => 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();
+ 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();
+ 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 options = null,
+ ActionBindingContext bindingContext = null)
+ {
+ var bindingContextAccessor = new ActionBindingContextAccessor();
+ if (bindingContext != null)
+ {
+ bindingContextAccessor.ActionBindingContext = bindingContext;
+ }
+
+ return new TestObjectResultExecutor(
+ options ?? new TestOptionsManager(),
+ 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 options,
+ IActionBindingContextAccessor bindingContextAccessor,
+ ILoggerFactory loggerFactory)
+ : base(options, bindingContextAccessor, loggerFactory)
+ {
+ }
+
+ public new IOutputFormatter SelectFormatter(
+ OutputFormatterContext formatterContext,
+ IList contentTypes,
+ IEnumerable formatters)
+ {
+ return base.SelectFormatter(formatterContext, contentTypes, formatters);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ObjectResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ObjectResultTests.cs
index bd15f3d5ec..af395781da 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ObjectResultTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ObjectResultTests.cs
@@ -3,977 +3,86 @@
using System;
using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
-using Microsoft.AspNet.Http;
-using Microsoft.AspNet.Http.Features;
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.AspNet.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.OptionsModel;
+using Microsoft.Extensions.Logging.Testing;
using Microsoft.Net.Http.Headers;
-using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class ObjectResultTests
{
- public static IEnumerable