diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 15549963e6..94c470e3a3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -250,6 +250,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(ArrayPool.Shared); services.TryAddSingleton(ArrayPool.Shared); + services.TryAddSingleton(); services.TryAddSingleton, ObjectResultExecutor>(); services.TryAddSingleton, PhysicalFileResultExecutor>(); services.TryAddSingleton, VirtualFileResultExecutor>(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultOutputFormatterSelector.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultOutputFormatterSelector.cs new file mode 100644 index 0000000000..e101061b0e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultOutputFormatterSelector.cs @@ -0,0 +1,322 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Formatters.Internal; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + public class DefaultOutputFormatterSelector : OutputFormatterSelector + { + private static readonly Comparison _sortFunction = (left, right) => + { + return left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1); + }; + + private readonly ILogger _logger; + private readonly IList _formatters; + private readonly bool _respectBrowserAcceptHeader; + private readonly bool _returnHttpNotAcceptable; + + public DefaultOutputFormatterSelector(IOptions options, ILoggerFactory loggerFactory) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _logger = loggerFactory.CreateLogger(); + + _formatters = new ReadOnlyCollection(options.Value.OutputFormatters); + _respectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader; + _returnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable; + } + + public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList formatters, MediaTypeCollection contentTypes) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (formatters == null) + { + throw new ArgumentNullException(nameof(formatters)); + } + + if (contentTypes == null) + { + throw new ArgumentNullException(nameof(contentTypes)); + } + + ValidateContentTypes(contentTypes); + + if (formatters.Count == 0) + { + formatters = _formatters; + if (formatters.Count == 0) + { + throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired( + typeof(MvcOptions).FullName, + nameof(MvcOptions.OutputFormatters), + typeof(IOutputFormatter).FullName)); + } + } + + var request = context.HttpContext.Request; + var acceptableMediaTypes = GetAcceptableMediaTypes(request); + var selectFormatterWithoutRegardingAcceptHeader = false; + + IOutputFormatter selectedFormatter = null; + if (acceptableMediaTypes.Count == 0) + { + // There is either no Accept header value, or it contained */* and we + // are not currently respecting the 'browser accept header'. + _logger.NoAcceptForNegotiation(); + + selectFormatterWithoutRegardingAcceptHeader = true; + } + else + { + if (contentTypes.Count == 0) + { + // Use whatever formatter can meet the client's request + selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( + context, + formatters, + acceptableMediaTypes); + } + else + { + // Verify that a content type from the context is compatible with the client's request + selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes( + context, + formatters, + acceptableMediaTypes, + contentTypes); + } + + if (selectedFormatter == null && !_returnHttpNotAcceptable) + { + _logger.NoFormatterFromNegotiation(acceptableMediaTypes); + + selectFormatterWithoutRegardingAcceptHeader = true; + } + } + + if (selectFormatterWithoutRegardingAcceptHeader) + { + if (contentTypes.Count == 0) + { + selectedFormatter = SelectFormatterNotUsingContentType( + context, + formatters); + } + else + { + selectedFormatter = SelectFormatterUsingAnyAcceptableContentType( + context, + formatters, + contentTypes); + } + } + + if (selectedFormatter == null) + { + // No formatter supports this. + _logger.NoFormatter(context); + return null; + } + + _logger.FormatterSelected(selectedFormatter, context); + return selectedFormatter; + } + + private List GetAcceptableMediaTypes(HttpRequest request) + { + var result = new List(); + AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result); + for (var i = 0; i < result.Count; i++) + { + var mediaType = new MediaType(result[i].MediaType); + if (!_respectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes) + { + result.Clear(); + return result; + } + } + + result.Sort(_sortFunction); + + return result; + } + + private IOutputFormatter SelectFormatterNotUsingContentType( + OutputFormatterCanWriteContext formatterContext, + IList formatters) + { + if (formatterContext == null) + { + throw new ArgumentNullException(nameof(formatterContext)); + } + + if (formatters == null) + { + throw new ArgumentNullException(nameof(formatters)); + } + + foreach (var formatter in formatters) + { + formatterContext.ContentType = new StringSegment(); + formatterContext.ContentTypeIsServerDefined = false; + + if (formatter.CanWriteResult(formatterContext)) + { + return formatter; + } + } + + return null; + } + + private IOutputFormatter SelectFormatterUsingSortedAcceptHeaders( + OutputFormatterCanWriteContext formatterContext, + IList formatters, + IList sortedAcceptHeaders) + { + if (formatterContext == null) + { + throw new ArgumentNullException(nameof(formatterContext)); + } + + if (formatters == null) + { + throw new ArgumentNullException(nameof(formatters)); + } + + if (sortedAcceptHeaders == null) + { + throw new ArgumentNullException(nameof(sortedAcceptHeaders)); + } + + for (var i = 0; i < sortedAcceptHeaders.Count; i++) + { + var mediaType = sortedAcceptHeaders[i]; + + formatterContext.ContentType = mediaType.MediaType; + formatterContext.ContentTypeIsServerDefined = false; + + for (var j = 0; j < formatters.Count; j++) + { + var formatter = formatters[j]; + if (formatter.CanWriteResult(formatterContext)) + { + return formatter; + } + } + } + + return null; + } + + private IOutputFormatter SelectFormatterUsingAnyAcceptableContentType( + OutputFormatterCanWriteContext formatterContext, + IList formatters, + MediaTypeCollection acceptableContentTypes) + { + if (formatterContext == null) + { + throw new ArgumentNullException(nameof(formatterContext)); + } + + if (formatters == null) + { + throw new ArgumentNullException(nameof(formatters)); + } + + if (acceptableContentTypes == null) + { + throw new ArgumentNullException(nameof(acceptableContentTypes)); + } + + foreach (var formatter in formatters) + { + foreach (var contentType in acceptableContentTypes) + { + formatterContext.ContentType = new StringSegment(contentType); + formatterContext.ContentTypeIsServerDefined = true; + + if (formatter.CanWriteResult(formatterContext)) + { + return formatter; + } + } + } + + return null; + } + + private IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes( + OutputFormatterCanWriteContext formatterContext, + IList formatters, + IList sortedAcceptableContentTypes, + MediaTypeCollection possibleOutputContentTypes) + { + for (var i = 0; i < sortedAcceptableContentTypes.Count; i++) + { + var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType); + for (var j = 0; j < possibleOutputContentTypes.Count; j++) + { + var candidateContentType = new MediaType(possibleOutputContentTypes[j]); + if (candidateContentType.IsSubsetOf(acceptableContentType)) + { + for (var k = 0; k < formatters.Count; k++) + { + var formatter = formatters[k]; + formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]); + formatterContext.ContentTypeIsServerDefined = true; + if (formatter.CanWriteResult(formatterContext)) + { + return formatter; + } + } + } + } + } + + return null; + } + + private void ValidateContentTypes(MediaTypeCollection contentTypes) + { + for (var i = 0; i < contentTypes.Count; i++) + { + var contentType = contentTypes[i]; + + var parsedContentType = new MediaType(contentType); + if (parsedContentType.HasWildcard) + { + var message = Resources.FormatObjectResult_MatchAllContentType( + contentType, + nameof(ObjectResult.ContentTypes)); + throw new InvalidOperationException(message); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ObjectResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ObjectResultExecutor.cs index fb767322a7..52cc84e846 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ObjectResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ObjectResultExecutor.cs @@ -3,19 +3,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.AspNetCore.Mvc.Formatters.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Infrastructure { @@ -27,17 +21,22 @@ public class ObjectResultExecutor : IActionResultExecutor /// /// Creates a new . /// - /// An accessor to . + /// The . /// The . /// The . public ObjectResultExecutor( - IOptions options, + OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, ILoggerFactory loggerFactory) { - if (options == null) + if (formatterSelector == null) { - throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(formatterSelector)); + } + + if (writerFactory == null) + { + throw new ArgumentNullException(nameof(writerFactory)); } if (loggerFactory == null) @@ -45,11 +44,9 @@ public ObjectResultExecutor( throw new ArgumentNullException(nameof(loggerFactory)); } - OptionsFormatters = options.Value.OutputFormatters; - RespectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader; - ReturnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable; - Logger = loggerFactory.CreateLogger(); + FormatterSelector = formatterSelector; WriterFactory = writerFactory.CreateWriter; + Logger = loggerFactory.CreateLogger(); } /// @@ -58,19 +55,9 @@ public ObjectResultExecutor( protected ILogger Logger { get; } /// - /// Gets the list of instances from . - /// - protected FormatterCollection OptionsFormatters { get; } - - /// - /// Gets the value of . - /// - protected bool RespectBrowserAcceptHeader { get; } - - /// - /// Gets the value of . + /// Gets the . /// - protected bool ReturnHttpNotAcceptable { get; } + protected OutputFormatterSelector FormatterSelector { get; } /// /// Gets the writer factory delegate. @@ -113,24 +100,6 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result) } } - ValidateContentTypes(result.ContentTypes); - - var formatters = result.Formatters; - if (formatters == null || formatters.Count == 0) - { - formatters = OptionsFormatters; - - // Complain about MvcOptions.OutputFormatters only if the result has an empty Formatters. - Debug.Assert(formatters != null, "MvcOptions.OutputFormatters cannot be null."); - if (formatters.Count == 0) - { - throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired( - typeof(MvcOptions).FullName, - nameof(MvcOptions.OutputFormatters), - typeof(IOutputFormatter).FullName)); - } - } - var objectType = result.DeclaredType; if (objectType == null || objectType == typeof(object)) { @@ -143,7 +112,10 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result) objectType, result.Value); - var selectedFormatter = SelectFormatter(formatterContext, result.ContentTypes, formatters); + var selectedFormatter = FormatterSelector.SelectFormatter( + formatterContext, + (IList)result.Formatters ?? Array.Empty(), + result.ContentTypes); if (selectedFormatter == null) { // No formatter supports this. @@ -153,334 +125,10 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result) return Task.CompletedTask; } - Logger.FormatterSelected(selectedFormatter, formatterContext); Logger.ObjectResultExecuting(context); 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( - OutputFormatterWriteContext formatterContext, - MediaTypeCollection contentTypes, - IList formatters) - { - if (formatterContext == null) - { - throw new ArgumentNullException(nameof(formatterContext)); - } - - if (contentTypes == null) - { - throw new ArgumentNullException(nameof(contentTypes)); - } - - if (formatters == null) - { - throw new ArgumentNullException(nameof(formatters)); - } - - var request = formatterContext.HttpContext.Request; - var acceptableMediaTypes = GetAcceptableMediaTypes(request); - var selectFormatterWithoutRegardingAcceptHeader = false; - IOutputFormatter selectedFormatter = null; - - if (acceptableMediaTypes.Count == 0) - { - // There is either no Accept header value, or it contained */* and we - // are not currently respecting the 'browser accept header'. - Logger.NoAcceptForNegotiation(); - - selectFormatterWithoutRegardingAcceptHeader = true; - } - else - { - if (contentTypes.Count == 0) - { - // Use whatever formatter can meet the client's request - selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( - formatterContext, - formatters, - acceptableMediaTypes); - } - else - { - // Verify that a content type from the context is compatible with the client's request - selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes( - formatterContext, - formatters, - acceptableMediaTypes, - contentTypes); - } - - if (selectedFormatter == null && !ReturnHttpNotAcceptable) - { - Logger.NoFormatterFromNegotiation(acceptableMediaTypes); - - selectFormatterWithoutRegardingAcceptHeader = true; - } - } - - if (selectFormatterWithoutRegardingAcceptHeader) - { - if (contentTypes.Count == 0) - { - selectedFormatter = SelectFormatterNotUsingContentType( - formatterContext, - formatters); - } - else - { - selectedFormatter = SelectFormatterUsingAnyAcceptableContentType( - formatterContext, - formatters, - contentTypes); - } - } - - return selectedFormatter; - } - - private List GetAcceptableMediaTypes( - HttpRequest request) - { - var result = new List(); - AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result); - for (var i = 0; i < result.Count; i++) - { - var mediaType = new MediaType(result[i].MediaType); - if (!RespectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes) - { - result.Clear(); - return result; - } - } - - result.Sort((left, right) => left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1)); - - return result; - } - - /// - /// 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 SelectFormatterNotUsingContentType( - OutputFormatterWriteContext formatterContext, - IList formatters) - { - if (formatterContext == null) - { - throw new ArgumentNullException(nameof(formatterContext)); - } - - if (formatters == null) - { - throw new ArgumentNullException(nameof(formatters)); - } - - foreach (var formatter in formatters) - { - formatterContext.ContentType = new StringSegment(); - formatterContext.ContentTypeIsServerDefined = false; - if (formatter.CanWriteResult(formatterContext)) - { - return formatter; - } - } - - return null; - } - - /// - /// 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( - OutputFormatterWriteContext formatterContext, - IList formatters, - IList sortedAcceptHeaders) - { - if (formatterContext == null) - { - throw new ArgumentNullException(nameof(formatterContext)); - } - - if (formatters == null) - { - throw new ArgumentNullException(nameof(formatters)); - } - - if (sortedAcceptHeaders == null) - { - throw new ArgumentNullException(nameof(sortedAcceptHeaders)); - } - - for (var i = 0; i < sortedAcceptHeaders.Count; i++) - { - var mediaType = sortedAcceptHeaders[i]; - formatterContext.ContentType = mediaType.MediaType; - formatterContext.ContentTypeIsServerDefined = false; - for (var j = 0; j < formatters.Count; j++) - { - var formatter = formatters[j]; - if (formatter.CanWriteResult(formatterContext)) - { - return formatter; - } - } - } - - return null; - } - - /// - /// 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( - OutputFormatterWriteContext formatterContext, - IList formatters, - MediaTypeCollection acceptableContentTypes) - { - if (formatterContext == null) - { - throw new ArgumentNullException(nameof(formatterContext)); - } - - if (formatters == null) - { - throw new ArgumentNullException(nameof(formatters)); - } - - if (acceptableContentTypes == null) - { - throw new ArgumentNullException(nameof(acceptableContentTypes)); - } - - foreach (var formatter in formatters) - { - foreach (var contentType in acceptableContentTypes) - { - formatterContext.ContentType = new StringSegment(contentType); - formatterContext.ContentTypeIsServerDefined = true; - if (formatter.CanWriteResult(formatterContext)) - { - return formatter; - } - } - } - - return null; - } - - /// - /// Selects the to write the response based on the content type values - /// present in and . - /// - /// The . - /// - /// The list of instances to consider. - /// - /// - /// The ordered content types from the Accept header, sorted by descending q-value. - /// - /// - /// The ordered content types from in descending priority order. - /// - /// - /// The selected or null if no formatter can write the response. - /// - protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes( - OutputFormatterWriteContext formatterContext, - IList formatters, - IList sortedAcceptableContentTypes, - MediaTypeCollection possibleOutputContentTypes) - { - for (var i = 0; i < sortedAcceptableContentTypes.Count; i++) - { - var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType); - for (var j = 0; j < possibleOutputContentTypes.Count; j++) - { - var candidateContentType = new MediaType(possibleOutputContentTypes[j]); - if (candidateContentType.IsSubsetOf(acceptableContentType)) - { - for (var k = 0; k < formatters.Count; k++) - { - var formatter = formatters[k]; - formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]); - formatterContext.ContentTypeIsServerDefined = true; - if (formatter.CanWriteResult(formatterContext)) - { - return formatter; - } - } - } - } - } - - return null; - } - - private void ValidateContentTypes(MediaTypeCollection contentTypes) - { - if (contentTypes == null) - { - return; - } - - for (var i = 0; i < contentTypes.Count; i++) - { - var contentType = contentTypes[i]; - var parsedContentType = new MediaType(contentType); - if (parsedContentType.HasWildcard) - { - var message = Resources.FormatObjectResult_MatchAllContentType( - contentType, - nameof(ObjectResult.ContentTypes)); - throw new InvalidOperationException(message); - } - } - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/OutputFormatterSelector.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/OutputFormatterSelector.cs new file mode 100644 index 0000000000..480775076d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/OutputFormatterSelector.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// Selects an to write a response to the current request. + /// + /// + /// + /// The default implementation of provided by ASP.NET Core MVC + /// is . The implements + /// MVC's default content negotiation algorthm. This API is designed in a way that can satisfy the contract + /// of . + /// + /// + /// The default implementation is controlled by settings on , most notably: + /// , , and + /// . + /// + /// + public abstract class OutputFormatterSelector + { + /// + /// Selects an to write the response based on the provided values and the current request. + /// + /// The associated with the current request. + /// A list of formatters to use; this acts as an override to . + /// A list of media types to use; this acts as an override to the Accept header. + /// The selected , or null if one could not be selected. + public abstract IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList formatters, MediaTypeCollection mediaTypes); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs index 3f49136162..7027dd64e5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs @@ -489,7 +489,7 @@ public static void ObjectResultExecuting(this ILogger logger, object value) public static void NoFormatter( this ILogger logger, - OutputFormatterWriteContext formatterContext) + OutputFormatterCanWriteContext formatterContext) { if (logger.IsEnabled(LogLevel.Warning)) { @@ -500,7 +500,7 @@ public static void NoFormatter( public static void FormatterSelected( this ILogger logger, IOutputFormatter outputFormatter, - OutputFormatterWriteContext context) + OutputFormatterCanWriteContext context) { if (logger.IsEnabled(LogLevel.Debug)) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs index 10e357988b..d6ab7ad228 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs @@ -276,7 +276,7 @@ private static IServiceProvider CreateServices(Mock formatter) options.Value.OutputFormatters.Add(formatter.Object); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs index 65d63f195f..2200131971 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs @@ -181,7 +181,7 @@ private static IServiceProvider CreateServices(Mock formatter) options.Value.OutputFormatters.Add(formatter.Object); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs index 25b7242ed9..8c32191534 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs @@ -141,7 +141,7 @@ private static IServiceProvider CreateServices(Mock formatter) options.Value.OutputFormatters.Add(formatter.Object); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtActionResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtActionResultTests.cs index 2a471e8566..5c21c139fc 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtActionResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtActionResultTests.cs @@ -98,7 +98,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtRouteResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtRouteResultTests.cs index be52aa4b29..9f19dc1794 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtRouteResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedAtRouteResultTests.cs @@ -113,7 +113,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedResultTests.cs index b8224e000d..84de33aed6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/CreatedResultTests.cs @@ -99,7 +99,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs index 46152bc1fa..6168694207 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpNotFoundObjectResultTest.cs @@ -78,7 +78,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpOkObjectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpOkObjectResultTest.cs index 7e9fe1bd94..db4d71f11f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpOkObjectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/HttpOkObjectResultTest.cs @@ -79,7 +79,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultOutputFormatterSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultOutputFormatterSelectorTest.cs new file mode 100644 index 0000000000..64310b8fd6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultOutputFormatterSelectorTest.cs @@ -0,0 +1,459 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + public class DefaultObjectResultExecutorTest + { + [Fact] + public void SelectFormatter_UsesPassedInFormatters_IgnoresOptionsFormatters() + { + // Arrange + var formatters = new List + { + new TestXmlOutputFormatter(), + new TestJsonOutputFormatter(), // This will be chosen based on the content type + }; + var selector = CreateSelector(new IOutputFormatter[] { }); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used + + // Act + var formatter = selector.SelectFormatter( + context, + formatters, + new MediaTypeCollection { "application/json" }); + + // Assert + Assert.Same(formatters[1], formatter); + Assert.Equal(new StringSegment("application/json"), context.ContentType); + } + + [Fact] + public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader() + { + // Arrange + var formatters = new List + { + new TestXmlOutputFormatter(), + new TestJsonOutputFormatter(), // This will be chosen based on the content type + }; + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection { "application/json" }); + + // Assert + Assert.Same(formatters[1], formatter); + Assert.Equal(new StringSegment("application/json"), context.ContentType); + } + + [Fact] + public void SelectFormatter_WithOneProvidedContentType_NoFallback() + { + // Arrange + var formatters = new List + { + new TestXmlOutputFormatter(), + }; + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection { "application/json" }); + + // Assert + Assert.Null(formatter); + } + + // ObjectResult.ContentTypes, Accept header, expected content type + public static TheoryData ContentTypes + { + get + { + var contentTypes = new MediaTypeCollection + { + "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( + MediaTypeCollection contentTypes, + string acceptHeader, + string expectedContentType) + { + // Arrange + var formatters = new List + { + new CannotWriteFormatter(), + new TestJsonOutputFormatter(), + }; + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader; + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + contentTypes); + + // Assert + Assert.Same(formatters[1], formatter); + Assert.Equal(new StringSegment(expectedContentType), context.ContentType); + } + + [Fact] + public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite() + { + // Arrange + var formatters = new List + { + new CannotWriteFormatter(), + new TestJsonOutputFormatter(), + new TestXmlOutputFormatter(), + }; + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection()); + + // Assert + Assert.Same(formatters[1], formatter); + Assert.Equal(new StringSegment("application/json"), context.ContentType); + } + + [Fact] + public void SelectFormatter_WithAcceptHeader_UsesFallback() + { + // Arrange + var formatters = new List + { + new TestXmlOutputFormatter(), + new TestJsonOutputFormatter(), + }; + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom"; + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection()); + + // Assert + Assert.Same(formatters[0], formatter); + Assert.Equal(new StringSegment("application/xml"), context.ContentType); + } + + [Fact] + public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback() + { + // Arrange + var options = new MvcOptions() + { + ReturnHttpNotAcceptable = true, + OutputFormatters = + { + new TestXmlOutputFormatter(), + new TestJsonOutputFormatter(), + }, + }; + + var selector = CreateSelector(options); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom"; + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection()); + + // Assert + Assert.Null(formatter); + } + + [Fact] + public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse() + { + // Arrange + var formatters = new List + { + new ServerContentTypeOnlyFormatter() + }; + + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom"; + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + new MediaTypeCollection()); + + // Assert + Assert.Null(formatter); + } + + [Fact] + public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected() + { + // Arrange + var formatters = new List + { + new ServerContentTypeOnlyFormatter() + }; + + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, text/custom2"; + + var serverDefinedContentTypes = new MediaTypeCollection(); + serverDefinedContentTypes.Add("text/other"); + serverDefinedContentTypes.Add("text/custom2"); + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + serverDefinedContentTypes); + + // Assert + Assert.Same(formatters[0], formatter); + Assert.Equal(new StringSegment("text/custom2"), context.ContentType); + } + + [Fact] + public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue() + { + // Arrange + var formatters = new List + { + new ServerContentTypeOnlyFormatter() + }; + + var selector = CreateSelector(formatters); + + var context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + new TestHttpResponseStreamWriterFactory().CreateWriter, + objectType: null, + @object: null); + + var serverDefinedContentTypes = new MediaTypeCollection(); + serverDefinedContentTypes.Add("text/custom"); + + // Act + var formatter = selector.SelectFormatter( + context, + Array.Empty(), + serverDefinedContentTypes); + + // Assert + Assert.Same(formatters[0], formatter); + Assert.Equal(new StringSegment("text/custom"), context.ContentType); + } + + private static DefaultOutputFormatterSelector CreateSelector(IEnumerable formatters) + { + var options = new MvcOptions(); + foreach (var formatter in formatters) + { + options.OutputFormatters.Add(formatter); + } + + return CreateSelector(options); + } + + private static DefaultOutputFormatterSelector CreateSelector(MvcOptions options) + { + return new DefaultOutputFormatterSelector(Options.Create(options), NullLoggerFactory.Instance); + } + + private class CannotWriteFormatter : IOutputFormatter + { + public virtual bool CanWriteResult(OutputFormatterCanWriteContext context) + { + return false; + } + + public virtual Task WriteAsync(OutputFormatterWriteContext context) + { + throw new NotImplementedException(); + } + } + + private class TestJsonOutputFormatter : TextOutputFormatter + { + public TestJsonOutputFormatter() + { + SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json")); + + SupportedEncodings.Add(Encoding.UTF8); + } + + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + return Task.FromResult(0); + } + } + + private class TestXmlOutputFormatter : TextOutputFormatter + { + public TestXmlOutputFormatter() + { + SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml")); + + SupportedEncodings.Add(Encoding.UTF8); + } + + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + return Task.FromResult(0); + } + } + + private class TestStringOutputFormatter : TextOutputFormatter + { + public TestStringOutputFormatter() + { + SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); + + SupportedEncodings.Add(Encoding.UTF8); + } + + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + return Task.FromResult(0); + } + } + + private class ServerContentTypeOnlyFormatter : OutputFormatter + { + public override bool CanWriteResult(OutputFormatterCanWriteContext context) + { + // This test formatter matches if and only if the content type is specified + // as "server defined". This lets tests identify what value the ObjectResultExecutor + // passed for that flag. + return context.ContentTypeIsServerDefined; + } + + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context) + { + return Task.FromResult(0); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs index ffdbdaa537..2cf6981202 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs @@ -21,48 +21,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { public class ObjectResultExecutorTest { - [Fact] - public void SelectFormatter_WithNoProvidedContentType_DoesConneg() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new TestXmlOutputFormatter(), - new TestJsonOutputFormatter(), // This will be chosen based on the accept header - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/json"; - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { "application/json" }, - formatters); - - // Assert - Assert.Same(formatters[1], formatter); - MediaTypeAssert.Equal("application/json", context.ContentType); - } - // For this test case probably the most common use case is when there is a format mapping based // content type selected but the developer had set the content type on the Response.ContentType [Fact] public async Task ExecuteAsync_ContentTypeProvidedFromResponseAndObjectResult_UsesResponseContentType() { // Arrange - var executor = CreateCustomObjectResultExecutor(); + var executor = CreateExecutor(); + var httpContext = new DefaultHttpContext(); var actionContext = new ActionContext() { HttpContext = httpContext }; httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used httpContext.Response.ContentType = "text/plain"; + var result = new ObjectResult("input"); result.Formatters.Add(new TestXmlOutputFormatter()); result.Formatters.Add(new TestJsonOutputFormatter()); @@ -72,50 +43,20 @@ public async Task ExecuteAsync_ContentTypeProvidedFromResponseAndObjectResult_Us await executor.ExecuteAsync(actionContext, result); // Assert - Assert.IsType(executor.SelectedOutputFormatter); MediaTypeAssert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType); } [Fact] - public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader() + public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_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 OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { "application/json" }, - formatters); - - // Assert - Assert.Same(formatters[1], formatter); - Assert.Equal(new StringSegment("application/json"), context.ContentType); - } - - [Fact] - public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_IgnoresAcceptHeader() - { - // Arrange - var executor = CreateCustomObjectResultExecutor(); var httpContext = new DefaultHttpContext(); var actionContext = new ActionContext() { HttpContext = httpContext }; httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used httpContext.Response.ContentType = "application/json"; + var result = new ObjectResult("input"); result.Formatters.Add(new TestXmlOutputFormatter()); result.Formatters.Add(new TestJsonOutputFormatter()); // This will be chosen based on the content type @@ -124,48 +65,20 @@ public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentTyp await executor.ExecuteAsync(actionContext, result); // Assert - Assert.IsType(executor.SelectedOutputFormatter); Assert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType); } [Fact] - public void SelectFormatter_WithOneProvidedContentType_NoFallback() + public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_NoFallback() { // Arrange var executor = CreateExecutor(); - var formatters = new List - { - new TestXmlOutputFormatter(), - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { "application/json" }, - formatters); - - // Assert - Assert.Null(formatter); - } - - [Fact] - public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_NoFallback() - { - // Arrange - var executor = CreateCustomObjectResultExecutor(); var httpContext = new DefaultHttpContext(); var actionContext = new ActionContext() { HttpContext = httpContext }; httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used httpContext.Response.ContentType = "application/json"; + var result = new ObjectResult("input"); result.Formatters.Add(new TestXmlOutputFormatter()); @@ -173,268 +86,7 @@ public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentTyp await executor.ExecuteAsync(actionContext, result); // Assert - Assert.Null(executor.SelectedOutputFormatter); - } - - // ObjectResult.ContentTypes, Accept header, expected content type - public static TheoryData ContentTypes - { - get - { - var contentTypes = new MediaTypeCollection - { - "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( - MediaTypeCollection contentTypes, - string acceptHeader, - string expectedContentType) - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new CannotWriteFormatter(), - new TestJsonOutputFormatter(), - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = acceptHeader; - - // Act - var formatter = executor.SelectFormatter( - context, - contentTypes, - formatters); - - // Assert - Assert.Same(formatters[1], formatter); - Assert.Equal(new StringSegment(expectedContentType), context.ContentType); - } - - [Fact] - public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new CannotWriteFormatter(), - new TestJsonOutputFormatter(), - new TestXmlOutputFormatter(), - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection(), - formatters); - - // Assert - Assert.Same(formatters[1], formatter); - Assert.Equal(new StringSegment("application/json"), context.ContentType); - } - - [Fact] - public void SelectFormatter_WithAcceptHeader_UsesFallback() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new TestXmlOutputFormatter(), - new TestJsonOutputFormatter(), - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom"; - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { }, - formatters); - - // Assert - Assert.Same(formatters[0], formatter); - Assert.Equal(new StringSegment("application/xml"), context.ContentType); - } - - [Fact] - public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback() - { - // Arrange - var options = Options.Create(new MvcOptions()); - options.Value.ReturnHttpNotAcceptable = true; - - var executor = CreateExecutor(options); - - var formatters = new List - { - new TestXmlOutputFormatter(), - new TestJsonOutputFormatter(), - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom"; - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { }, - formatters); - - // Assert - Assert.Null(formatter); - } - - [Fact] - public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new ServerContentTypeOnlyFormatter() - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom"; - - // Act - var formatter = executor.SelectFormatter( - context, - new MediaTypeCollection { }, - formatters); - - // Assert - Assert.Null(formatter); - } - - [Fact] - public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new ServerContentTypeOnlyFormatter() - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom, text/custom2"; - - var serverDefinedContentTypes = new MediaTypeCollection(); - serverDefinedContentTypes.Add("text/other"); - serverDefinedContentTypes.Add("text/custom2"); - - // Act - var formatter = executor.SelectFormatter( - context, - serverDefinedContentTypes, - formatters); - - // Assert - Assert.Same(formatters[0], formatter); - Assert.Equal(new StringSegment("text/custom2"), context.ContentType); - } - - [Fact] - public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue() - { - // Arrange - var executor = CreateExecutor(); - - var formatters = new List - { - new ServerContentTypeOnlyFormatter() - }; - - var context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - new TestHttpResponseStreamWriterFactory().CreateWriter, - objectType: null, - @object: null); - - var serverDefinedContentTypes = new MediaTypeCollection(); - serverDefinedContentTypes.Add("text/custom"); - - // Act - var formatter = executor.SelectFormatter( - context, - serverDefinedContentTypes, - formatters); - - // Assert - Assert.Same(formatters[0], formatter); - Assert.Equal(new StringSegment("text/custom"), context.ContentType); + Assert.Equal(406, httpContext.Response.StatusCode); } [Fact] @@ -637,20 +289,10 @@ private static HttpContext GetHttpContext() return httpContext; } - private static TestObjectResultExecutor CreateExecutor(IOptions options = null) - { - return new TestObjectResultExecutor( - options ?? Options.Create(new MvcOptions()), - new TestHttpResponseStreamWriterFactory(), - NullLoggerFactory.Instance); - } - - private static CustomObjectResultExecutor CreateCustomObjectResultExecutor() + private static ObjectResultExecutor CreateExecutor(IOptions options = null) { - return new CustomObjectResultExecutor( - Options.Create(new MvcOptions()), - new TestHttpResponseStreamWriterFactory(), - NullLoggerFactory.Instance); + var selector = new DefaultOutputFormatterSelector(options ?? Options.Create(new MvcOptions()), NullLoggerFactory.Instance); + return new ObjectResultExecutor(selector, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance); } private class CannotWriteFormatter : IOutputFormatter @@ -713,47 +355,6 @@ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, } } - private class TestObjectResultExecutor : ObjectResultExecutor - { - public TestObjectResultExecutor( - IOptions options, - IHttpResponseStreamWriterFactory writerFactory, - ILoggerFactory loggerFactory) - : base(options, writerFactory, loggerFactory) - { - } - - new public IOutputFormatter SelectFormatter( - OutputFormatterWriteContext formatterContext, - MediaTypeCollection contentTypes, - IList formatters) - { - return base.SelectFormatter(formatterContext, contentTypes, formatters); - } - } - - private class CustomObjectResultExecutor : ObjectResultExecutor - { - public CustomObjectResultExecutor( - IOptions options, - IHttpResponseStreamWriterFactory writerFactory, - ILoggerFactory loggerFactory) - : base(options, writerFactory, loggerFactory) - { - } - - public IOutputFormatter SelectedOutputFormatter { get; private set; } - - protected override IOutputFormatter SelectFormatter( - OutputFormatterWriteContext formatterContext, - MediaTypeCollection contentTypes, - IList formatters) - { - SelectedOutputFormatter = base.SelectFormatter(formatterContext, contentTypes, formatters); - return SelectedOutputFormatter; - } - } - private class ServerContentTypeOnlyFormatter : OutputFormatter { public override bool CanWriteResult(OutputFormatterCanWriteContext context) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 4723e2e66c..8570830236 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -1596,14 +1596,14 @@ private ControllerActionInvoker CreateInvoker( } var httpContext = new DefaultHttpContext(); - var options = new MvcOptions(); - var mvcOptionsAccessor = Options.Create(options); + + var options = Options.Create(new MvcOptions()); var services = new ServiceCollection(); services.AddSingleton(NullLoggerFactory.Instance); - services.AddSingleton>(mvcOptionsAccessor); + services.AddSingleton>(options); services.AddSingleton>(new ObjectResultExecutor( - mvcOptionsAccessor, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); @@ -1622,7 +1622,7 @@ private ControllerActionInvoker CreateInvoker( await c.HttpContext.Response.WriteAsync(c.Object.ToString()); }); - options.OutputFormatters.Add(formatter.Object); + options.Value.OutputFormatters.Add(formatter.Object); var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); if (diagnosticListener != null) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ObjectResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ObjectResultTests.cs index fe8ce3c15c..2c0e9ab003 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ObjectResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ObjectResultTests.cs @@ -65,7 +65,7 @@ private static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - Options.Create(new MvcOptions()), + new DefaultOutputFormatterSelector(Options.Create(new MvcOptions()), NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); services.AddSingleton(NullLoggerFactory.Instance); diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/BadRequestErrorMessageResultTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/BadRequestErrorMessageResultTest.cs index 818c6fd072..eb625745db 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/BadRequestErrorMessageResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/BadRequestErrorMessageResultTest.cs @@ -76,7 +76,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/ExceptionResultTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/ExceptionResultTest.cs index 26c457e5ff..60d40dd418 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/ExceptionResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/ExceptionResultTest.cs @@ -76,7 +76,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/InvalidModelStateResultTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/InvalidModelStateResultTest.cs index 472321d826..51fee66615 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/InvalidModelStateResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/InvalidModelStateResultTest.cs @@ -89,7 +89,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/NegotiatedContentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/NegotiatedContentResultTest.cs index 6d650e2486..1e8011e116 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/NegotiatedContentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/NegotiatedContentResultTest.cs @@ -77,7 +77,7 @@ private static IServiceProvider CreateServices() var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( - options, + new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance));