From 06cc58663ece44e9e0143cb837bb4e5598abf4c2 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 14 Oct 2015 21:36:23 -0700 Subject: [PATCH] Move implementation of ObjectResult into a facade --- .../Formatters/OutputFormatterContext.cs | 4 - .../CreatedAtActionResult.cs | 4 +- .../CreatedAtRouteResult.cs | 4 +- .../CreatedResult.cs | 4 +- .../MvcCoreServiceCollectionExtensions.cs | 1 + .../HttpNoContentOutputFormatter.cs | 7 +- .../Infrastructure/ObjectResultExecutor.cs | 432 ++++++++ src/Microsoft.AspNet.Mvc.Core/ObjectResult.cs | 278 +---- .../CreatedAtActionResultTests.cs | 49 +- .../CreatedAtRouteResultTests.cs | 39 +- .../CreatedResultTests.cs | 47 +- .../HttpNotAcceptableOutputFormatterTest.cs | 69 ++ .../Formatters/NoContentFormatterTests.cs | 46 +- .../HttpNotFoundObjectResultTest.cs | 102 +- .../HttpOkObjectResultTest.cs | 27 +- .../ObjectResultExecutorTest.cs | 502 +++++++++ .../ObjectResultTests.cs | 959 +----------------- 17 files changed, 1180 insertions(+), 1394 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Formatters/HttpNotAcceptableOutputFormatterTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs 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 ContentTypes - { - get - { - var contentTypes = new string[] - { - "text/plain", - "text/xml", - "application/json", - }; - - // Empty accept header, should select based on contentTypes. - yield return new object[] { contentTypes, "", "application/json; charset=utf-8" }; - - // null accept header, should select based on contentTypes. - yield return new object[] { contentTypes, null, "application/json; charset=utf-8" }; - - // No accept Header match with given contentype collection. - // Should select based on if any formatter supported any content type. - yield return new object[] { contentTypes, "text/custom", "application/json; charset=utf-8" }; - - // Accept Header matches but no formatter supports the accept header. - // Should select based on if any formatter supported any user provided content type. - yield return new object[] { contentTypes, "text/xml", "application/json; charset=utf-8" }; - - // Filtets out Accept headers with 0 quality and selects the one with highest quality. - yield return new object[] - { - contentTypes, - "text/plain;q=0.3, text/json;q=0, text/cusotm;q=0.0, application/json;q=0.4", - "application/json; charset=utf-8" - }; - } - } - - [Theory] - [MemberData(nameof(ContentTypes))] - public async Task ObjectResult_WithMultipleContentTypesAndAcceptHeaders_PerformsContentNegotiation( - IEnumerable contentTypes, string acceptHeader, string expectedHeader) - { - // Arrange - var expectedContentType = expectedHeader; - var input = "testInput"; - 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, acceptHeader); - - var result = new ObjectResult(input); - - // Set the content type property explicitly. - result.ContentTypes = contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)).ToList(); - result.Formatters = new List - { - new CannotWriteFormatter(), - new JsonOutputFormatter(), - }; - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // should always select the Json Output formatter even though it is second in the list. - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - [Fact] - public void ObjectResult_Create_CallsContentResult_InitializesValue() + public void ObjectResult_Constructor() { // Arrange var input = "testInput"; - var actionContext = CreateMockActionContext(); // Act var result = new ObjectResult(input); // Assert Assert.Equal(input, result.Value); + Assert.Empty(result.ContentTypes); + Assert.Empty(result.Formatters); + Assert.Null(result.StatusCode); + Assert.Null(result.DeclaredType); } [Fact] - public async Task NoAcceptAndContentTypeHeaders_406Formatter_DoesNotTakeEffect() + public async Task ObjectResult_ExecuteResultAsync_SetsStatusCode() { // Arrange - var expectedContentType = "application/json; charset=utf-8"; - - var input = 123; - var httpResponse = new DefaultHttpContext().Response; - httpResponse.Body = new MemoryStream(); - var actionContext = CreateMockActionContext( - outputFormatters: new IOutputFormatter[] + var result = new ObjectResult("Hello") + { + StatusCode = 404, + Formatters = new List() { - new HttpNotAcceptableOutputFormatter(), - new JsonOutputFormatter() + new NoOpOutputFormatter(), }, - response: httpResponse, - requestAcceptHeader: null, - requestContentType: null, - requestAcceptCharsetHeader: null); + }; - var result = new ObjectResult(input); - result.ContentTypes = new List(); - result.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json")); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expectedContentType, httpResponse.ContentType); - } - - [Fact] - public async Task ObjectResult_WithSingleContentType_TheGivenContentTypeIsSelected() - { - // Arrange - var expectedContentType = "application/json; charset=utf-8"; - - // non string value. - var input = 123; - var httpResponse = new DefaultHttpContext().Response; - httpResponse.Body = new MemoryStream(); - var actionContext = CreateMockActionContext(httpResponse); - - // Set the content type property explicitly to a single value. - var result = new ObjectResult(input); - result.ContentTypes = new List(); - result.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json")); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expectedContentType, httpResponse.ContentType); - } - - [Fact] - public async Task ObjectResult_FallsBackOn_FormattersInOptions() - { - // Arrange - var formatter = GetMockFormatter(); - var actionContext = CreateMockActionContext( - new[] { formatter.Object }, - setupActionBindingContext: false); - - // Set the content type property explicitly to a single value. - var result = new ObjectResult("someValue"); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - formatter.Verify(o => o.WriteAsync(It.IsAny())); - } - - [Fact] - public async Task ObjectResult_WithSingleContentType_TheContentTypeIsIgnoredIfTheTypeIsString() - { - // Arrange - var contentType = "application/json;charset=utf-8"; - var expectedContentType = "text/plain; charset=utf-8"; - - // string value. - var input = "1234"; - var httpResponse = GetMockHttpResponse(); - var actionContext = CreateMockActionContext(httpResponse.Object); - - // Set the content type property explicitly to a single value. - var result = new ObjectResult(input); - result.ContentTypes = new List(); - result.ContentTypes.Add(MediaTypeHeaderValue.Parse(contentType)); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - - [Fact] - public async Task ObjectResult_MultipleContentTypes_PicksFirstFormatterWhichSupportsAnyOfTheContentTypes() - { - // Arrange - var expectedContentType = "application/json; charset=utf-8"; - var input = "testInput"; - var httpResponse = GetMockHttpResponse(); - var actionContext = CreateMockActionContext(httpResponse.Object, requestAcceptHeader: null); - var result = new ObjectResult(input); - - // It should not select TestOutputFormatter, - // This is because it should accept the first formatter which supports any of the two contentTypes. - var contentTypes = new[] { "application/custom", "application/json" }; - - // Set the content type and the formatters property explicitly. - result.ContentTypes = contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)) - .ToList(); - result.Formatters = new List - { - new CannotWriteFormatter(), - new JsonOutputFormatter(), - }; - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Asserts that content type is not text/custom. - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - - [Fact] - public async Task ObjectResult_MultipleFormattersSupportingTheSameContentType_SelectsTheFirstFormatterInList() - { - // Arrange - var input = "testInput"; - var stream = new MemoryStream(); - - var httpResponse = GetMockHttpResponse(); - var actionContext = CreateMockActionContext(httpResponse.Object, requestAcceptHeader: null); - var result = new ObjectResult(input); - - // It should select the mock formatter as that is the first one in the list. - var contentTypes = new[] { "application/json", "text/custom" }; - var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse("text/custom"); - - // Get a mock formatter which supports everything. - var mockFormatter = GetMockFormatter(); - - result.ContentTypes = contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)).ToList(); - result.Formatters = new List - { - mockFormatter.Object, - new JsonOutputFormatter(), - new CannotWriteFormatter() - }; - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Verify that mock formatter was chosen. - mockFormatter.Verify(o => o.WriteAsync(It.IsAny())); - } - - [Fact] - public async Task ObjectResult_NoContentTypeSetWithAcceptHeaders_PicksFormatterOnAcceptHeaders() - { - // Arrange - var expectedContentType = "application/json; charset=utf-8"; - var input = "testInput"; - var stream = new MemoryStream(); - - var httpResponse = GetMockHttpResponse(); - var actionContext = - CreateMockActionContext(httpResponse.Object, - requestAcceptHeader: "text/custom;q=0.1,application/json;q=0.9", - requestContentType: "application/custom"); - var result = new ObjectResult(input); - - // Set more than one formatters. The test output formatter throws on write. - result.Formatters = new List - { - new CannotWriteFormatter(), - new JsonOutputFormatter(), - }; - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Asserts that content type is not text/custom. i.e the formatter is not TestOutputFormatter. - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - - [Fact] - public async Task ObjectResult_NoContentTypeSetWithNoAcceptHeaders_PicksFormatterOnRequestContentType() - { - // Arrange - var stream = new MemoryStream(); - var expectedContentType = "application/json; charset=utf-8"; - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var actionContext = CreateMockActionContext(httpResponse.Object, - requestAcceptHeader: null, - requestContentType: "application/json"); - var input = "testInput"; - var result = new ObjectResult(input); - - // Set more than one formatters. The test output formatter throws on write. - result.Formatters = new List - { - new CannotWriteFormatter(), - new JsonOutputFormatter(), - }; - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Asserts that content type is not text/custom. - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("application/xml")] - [InlineData("application/custom")] - [InlineData("application/xml;q=1, application/custom;q=0.8")] - public void SelectFormatter_WithNoMatchingAcceptHeader_PicksFormatterBasedOnObjectType(string acceptHeader) - { - // For no Accept headers, CanWriteResult is called once for the type match pass. For each additional Accept - // header, it is called once. - // Arrange - var acceptHeaderCollection = string.IsNullOrEmpty(acceptHeader) ? - null : - MediaTypeHeaderValue.ParseList(new[] { acceptHeader }).ToArray(); - var stream = new MemoryStream(); - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var actionContext = CreateMockActionContext( - httpResponse.Object, - requestAcceptHeader: acceptHeader, - requestContentType: "application/text"); - var input = "testInput"; - var result = new ObjectResult(input); - var mockCountingFormatter = new Mock(); - - var context = new OutputFormatterContext() + var actionContext = new ActionContext() { - HttpContext = actionContext.HttpContext, - Object = input, - DeclaredType = typeof(string) + HttpContext = new DefaultHttpContext() + { + RequestServices = CreateServices(), + } }; - var requestContentType = MediaTypeHeaderValue.Parse("application/text"); - mockCountingFormatter - .Setup(o => o.CanWriteResult(context, It.Is(mth => mth == null))) - .Returns(true); - mockCountingFormatter - .Setup(o => o.CanWriteResult(context, requestContentType)) - .Returns(true); - - // Set more than one formatters. The test output formatter throws on write. - result.Formatters = new List - { - new CannotWriteFormatter(), - mockCountingFormatter.Object, - }; - - // Act - var formatter = result.SelectFormatter(context, result.Formatters); - - // Assert - Assert.Equal(mockCountingFormatter.Object, formatter); - - // CanWriteResult is called once for the type-based match. - mockCountingFormatter.Verify(v => v.CanWriteResult(context, null), Times.Once()); - - // CanWriteResult is never called for the request's Content-Type. - mockCountingFormatter.Verify(v => v.CanWriteResult(context, requestContentType), Times.Never()); - - // In total, CanWriteResult is invoked for the following cases: - // 1. For each Accept header present - // 2. Type-based match - var callCount = (acceptHeaderCollection == null ? 0 : acceptHeaderCollection.Count()) + 1; - mockCountingFormatter.Verify( - v => v.CanWriteResult(It.IsAny< OutputFormatterContext>(), It.IsAny()), - Times.Exactly(callCount)); - } - - [Fact] - public async Task - ObjectResult_NoContentTypeSetWithNoAcceptHeadersAndNoRequestContentType_PicksFirstFormatterWhichCanWrite() - { - // Arrange - var stream = new MemoryStream(); - var expectedContentType = "application/json; charset=utf-8"; - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var actionContext = CreateMockActionContext(httpResponse.Object, - requestAcceptHeader: null, - requestContentType: null); - var input = "testInput"; - var result = new ObjectResult(input); - - // Set more than one formatters. The test output formatter throws on write. - result.Formatters = new List - { - new CannotWriteFormatter(), - new JsonOutputFormatter(), - }; - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Asserts that content type is not text/custom. - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - } - - [Fact] - public async Task ObjectResult_NoFormatterFound_Returns406() - { - // Arrange - var stream = new MemoryStream(); - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var actionContext = CreateMockActionContext(httpResponse.Object, - requestAcceptHeader: null, - requestContentType: null); - var input = "testInput"; - var result = new ObjectResult(input); - - // Set more than one formatters. The test output formatter throws on write. - result.Formatters = new List - { - new CannotWriteFormatter(), - }; - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Asserts that content type is not text/custom. - httpResponse.VerifySet(r => r.StatusCode = StatusCodes.Status406NotAcceptable); - } - - [Fact] - public async Task ObjectResult_Execute_CallsContentResult_SetsContent() - { - // Arrange - var expectedContentType = "text/plain; charset=utf-8"; - var input = "testInput"; - var stream = new MemoryStream(); - - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var actionContext = CreateMockActionContext(httpResponse.Object, - requestAcceptHeader: null, - requestContentType: null); - - // Act - var result = new ObjectResult(input); - await result.ExecuteResultAsync(actionContext); - - // Assert - httpResponse.VerifySet(r => r.ContentType = expectedContentType); - - // The following verifies the correct Content was written to Body - Assert.Equal(input.Length, httpResponse.Object.Body.Length); - } - - [Fact] - public async Task ObjectResult_Execute_NullContent_SetsStatusCode() - { - // Arrange - var stream = new MemoryStream(); - var expectedStatusCode = StatusCodes.Status201Created; - var httpResponse = new Mock(); - httpResponse.SetupGet(r => r.Body).Returns(stream); - - var formatters = new IOutputFormatter[] - { - new HttpNoContentOutputFormatter(), - new StringOutputFormatter(), - new JsonOutputFormatter() - }; - var actionContext = CreateMockActionContext(formatters, - httpResponse.Object, - requestAcceptHeader: null, - requestContentType: null); - var result = new ObjectResult(null); - result.StatusCode = expectedStatusCode; // Act await result.ExecuteResultAsync(actionContext); // Assert - httpResponse.VerifySet(r => r.StatusCode = expectedStatusCode); - Assert.Equal(0, httpResponse.Object.Body.Length); + Assert.Equal(404, actionContext.HttpContext.Response.StatusCode); } - [Fact] - public async Task ObjectResult_Execute_CallsJsonResult_SetsContent() + private static IServiceProvider CreateServices() { - // Arrange - var expectedContentType = "application/json; charset=utf-8"; - var nonStringValue = new { x1 = 10, y1 = "Hello" }; - var httpResponse = Mock.Of(); - httpResponse.Body = new MemoryStream(); - var actionContext = CreateMockActionContext(httpResponse); - var tempStream = new MemoryStream(); - var tempHttpContext = new Mock(); - var tempHttpResponse = new Mock(); + var services = new ServiceCollection(); + services.AddInstance(new ObjectResultExecutor( + new TestOptionsManager(), + new ActionBindingContextAccessor(), + NullLoggerFactory.Instance)); - tempHttpResponse.SetupGet(o => o.Body).Returns(tempStream); - tempHttpResponse.SetupProperty(o => o.ContentType); - tempHttpContext.SetupGet(o => o.Request).Returns(new DefaultHttpContext().Request); - tempHttpContext.SetupGet(o => o.Response).Returns(tempHttpResponse.Object); - var tempActionContext = new ActionContext(tempHttpContext.Object, - new RouteData(), - new ActionDescriptor()); - var formatterContext = new OutputFormatterContext() + return services.BuildServiceProvider(); + } + + private class NoOpOutputFormatter : IOutputFormatter + { + public bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) { - HttpContext = tempActionContext.HttpContext, - Object = nonStringValue, - DeclaredType = nonStringValue.GetType() - }; - var formatter = new JsonOutputFormatter(); - formatter.WriteResponseHeaders(formatterContext); - await formatter.WriteAsync(formatterContext); - - // Act - var result = new ObjectResult(nonStringValue); - await result.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expectedContentType, httpResponse.ContentType); - Assert.Equal(tempStream.ToArray(), ((MemoryStream)actionContext.HttpContext.Response.Body).ToArray()); - } - - [Theory] - [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "application/json; charset=utf-8")] // Chrome & Opera - [InlineData("text/html, application/xhtml+xml, */*", - "application/json; charset=utf-8")] //IE - [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "application/json; charset=utf-8")] // Firefox & Safari - [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 ObjectResult_SelectDefaultFormatter_OnAllMediaRangeAcceptHeaderMediaType( - string acceptHeader, - string expectedResponseContentType) - { - // Arrange - var objectResult = new ObjectResult(new Person() { Name = "John" }); - var outputFormatters = new IOutputFormatter[] { - new HttpNoContentOutputFormatter(), - new StringOutputFormatter(), - new JsonOutputFormatter(), - new XmlDataContractSerializerOutputFormatter() - }; - var response = GetMockHttpResponse(); - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: acceptHeader); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(resp => resp.ContentType = expectedResponseContentType); - } - - [ConditionalTheory] - // Mono issue - https://github.com/aspnet/External/issues/18 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "application/xml; charset=utf-8")] // Chrome & Opera - [InlineData("text/html, application/xhtml+xml, */*", - "application/json; charset=utf-8")] //IE - [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "application/xml; charset=utf-8")] // Firefox & Safari - [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 expectedResponseContentType) - { - // Arrange - var objectResult = new ObjectResult(new Person() { Name = "John" }); - var outputFormatters = new IOutputFormatter[] { - new HttpNoContentOutputFormatter(), - new StringOutputFormatter(), - new JsonOutputFormatter(), - new XmlDataContractSerializerOutputFormatter() - }; - var response = GetMockHttpResponse(); - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: acceptHeader, - respectBrowserAcceptHeader: true); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(resp => resp.ContentType = expectedResponseContentType); - } - - [ConditionalTheory] - // Mono issue - https://github.com/aspnet/External/issues/18 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - [InlineData("application/xml;q=0.9,text/plain;q=0.5", "application/xml; charset=utf-8", false)] - [InlineData("application/xml;q=0.9,*/*;q=0.5", "application/json; charset=utf-8", false)] - [InlineData("application/xml;q=0.9,text/plain;q=0.5", "application/xml; charset=utf-8", true)] - [InlineData("application/xml;q=0.9,*/*;q=0.5", "application/xml; charset=utf-8", true)] - public async Task ObjectResult_WildcardAcceptMediaType_AndExplicitResponseContentType( - string acceptHeader, - string expectedResponseContentType, - bool respectBrowserAcceptHeader) - { - // Arrange - var objectResult = new ObjectResult(new Person() { Name = "John" }); - objectResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/xml")); - objectResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json")); - var outputFormatters = new IOutputFormatter[] { - new HttpNoContentOutputFormatter(), - new StringOutputFormatter(), - new JsonOutputFormatter(), - new XmlDataContractSerializerOutputFormatter() - }; - var response = GetMockHttpResponse(); - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - acceptHeader, - respectBrowserAcceptHeader: respectBrowserAcceptHeader); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(resp => resp.ContentType = expectedResponseContentType); - } - - [Theory] - [InlineData("application/*", "application/*")] - [InlineData("application/xml, application/*, application/json", "application/*")] - [InlineData("application/*, application/json", "application/*")] - - [InlineData("*/*", "*/*")] - [InlineData("application/xml, */*, application/json", "*/*")] - [InlineData("*/*, application/json", "*/*")] - public async Task ObjectResult_MatchAllContentType_Throws(string content, string invalidContentType) - { - // Arrange - var contentTypes = content.Split(','); - var objectResult = new ObjectResult(new Person() { Name = "John" }); - objectResult.ContentTypes = contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)) - .ToList(); - var actionContext = CreateMockActionContext(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => objectResult.ExecuteResultAsync(actionContext)); - - 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); - } - - [Fact] - public async Task ObjectResult_WithStringType_WritesTextPlain_Ignoring406Formatter() - { - // Arrange - var expectedData = "Hello World!"; - var objectResult = new ObjectResult(expectedData); - var outputFormatters = new IOutputFormatter[] - { - new HttpNotAcceptableOutputFormatter(), - new StringOutputFormatter(), - new JsonOutputFormatter() - }; - - var response = new Mock(); - var responseStream = new MemoryStream(); - response.SetupGet(r => r.Body).Returns(responseStream); - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: "application/json"); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(r => r.ContentType = "text/plain; charset=utf-8"); - responseStream.Position = 0; - var actual = new StreamReader(responseStream).ReadToEnd(); - Assert.Equal(expectedData, actual); - } - - [Fact] - public async Task ObjectResult_WithSingleContentType_Ignores406Formatter() - { - // Arrange - var objectResult = new ObjectResult(new Person() { Name = "John" }); - objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/json")); - var outputFormatters = new IOutputFormatter[] - { - new HttpNotAcceptableOutputFormatter(), - new JsonOutputFormatter() - }; - var response = new Mock(); - var responseStream = new MemoryStream(); - response.SetupGet(r => r.Body).Returns(responseStream); - var expectedData = "{\"Name\":\"John\"}"; - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: "application/non-existing", - requestContentType: "application/non-existing"); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(r => r.ContentType = "application/json; charset=utf-8"); - responseStream.Position = 0; - var actual = new StreamReader(responseStream).ReadToEnd(); - Assert.Equal(expectedData, actual); - } - - [Fact] - public async Task ObjectResult_WithMultipleContentTypes_Ignores406Formatter() - { - // Arrange - var objectResult = new ObjectResult(new Person() { Name = "John" }); - objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/foo")); - objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/json")); - var outputFormatters = new IOutputFormatter[] - { - new HttpNotAcceptableOutputFormatter(), - new JsonOutputFormatter() - }; - var response = new Mock(); - var responseStream = new MemoryStream(); - response.SetupGet(r => r.Body).Returns(responseStream); - var expectedData = "{\"Name\":\"John\"}"; - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: "application/non-existing", - requestContentType: "application/non-existing"); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(r => r.ContentType = "application/json; charset=utf-8"); - responseStream.Position = 0; - var actual = new StreamReader(responseStream).ReadToEnd(); - Assert.Equal(expectedData, actual); - } - - [Fact] - public async Task ObjectResult_WithStream_DoesNotSetContentType_IfNotProvided() - { - // Arrange - var objectResult = new ObjectResult(new MemoryStream(Encoding.UTF8.GetBytes("Name=James"))); - var outputFormatters = new IOutputFormatter[] - { - new StreamOutputFormatter(), - new JsonOutputFormatter() - }; - var response = new Mock(); - var responseStream = new MemoryStream(); - response.SetupGet(r => r.Body).Returns(responseStream); - var expectedData = "Name=James"; - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: null, - requestContentType: null); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(r => r.ContentType = It.IsAny(), Times.Never()); - responseStream.Position = 0; - var actual = new StreamReader(responseStream).ReadToEnd(); - Assert.Equal(expectedData, actual); - } - - [Fact] - public async Task ObjectResult_WithStream_SetsExplicitContentType() - { - // Arrange - var objectResult = new ObjectResult(new MemoryStream(Encoding.UTF8.GetBytes("Name=James"))); - objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/foo")); - var outputFormatters = new IOutputFormatter[] - { - new StreamOutputFormatter(), - new JsonOutputFormatter() - }; - var response = new Mock(); - var responseStream = new MemoryStream(); - response.SetupGet(r => r.Body).Returns(responseStream); - var expectedData = "Name=James"; - - var actionContext = CreateMockActionContext( - outputFormatters, - response.Object, - requestAcceptHeader: "application/json", - requestContentType: null); - - // Act - await objectResult.ExecuteResultAsync(actionContext); - - // Assert - response.VerifySet(r => r.ContentType = "application/foo"); - responseStream.Position = 0; - var actual = new StreamReader(responseStream).ReadToEnd(); - Assert.Equal(expectedData, actual); - } - - private static ActionContext CreateMockActionContext( - HttpResponse response = null, - string requestAcceptHeader = "application/*", - string requestContentType = "application/json", - string requestAcceptCharsetHeader = "", - bool respectBrowserAcceptHeader = false) - { - var formatters = new IOutputFormatter[] { new StringOutputFormatter(), new JsonOutputFormatter() }; - - return CreateMockActionContext( - formatters, - response: response, - requestAcceptHeader: requestAcceptHeader, - requestContentType: requestContentType, - requestAcceptCharsetHeader: requestAcceptCharsetHeader, - respectBrowserAcceptHeader: respectBrowserAcceptHeader); - } - - private static ActionContext CreateMockActionContext( - IEnumerable outputFormatters, - HttpResponse response = null, - string requestAcceptHeader = "application/*", - string requestContentType = "application/json", - string requestAcceptCharsetHeader = "", - bool respectBrowserAcceptHeader = false, - bool setupActionBindingContext = true) - { - var httpContext = new Mock(); - if (response != null) - { - httpContext.Setup(o => o.Response).Returns(response); + return true; } - 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.Features).Returns(new FeatureCollection()); - httpContext.Setup(o => o.Request).Returns(request); - httpContext.Setup(o => o.RequestServices).Returns(GetServiceProvider()); - - var optionsAccessor = new TestOptionsManager(); - foreach (var formatter in outputFormatters) + public Task WriteAsync(OutputFormatterContext context) { - optionsAccessor.Value.OutputFormatters.Add(formatter); + return Task.FromResult(0); } - - optionsAccessor.Value.RespectBrowserAcceptHeader = respectBrowserAcceptHeader; - httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) - .Returns(optionsAccessor); - httpContext.Setup(o => o.RequestServices.GetService(typeof(ILogger))) - .Returns(new Mock>().Object); - - ActionBindingContext actionBindingContext = null; - if (setupActionBindingContext) - { - actionBindingContext = new ActionBindingContext { OutputFormatters = outputFormatters.ToList() }; - } - - httpContext.Setup(o => o.RequestServices.GetService(typeof(IActionBindingContextAccessor))) - .Returns(new ActionBindingContextAccessor() { ActionBindingContext = actionBindingContext }); - - return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); - } - - private static Mock GetMockHttpResponse() - { - var stream = new MemoryStream(); - var httpResponse = new Mock(); - httpResponse.SetupProperty(o => o.ContentType); - httpResponse.SetupGet(r => r.Body).Returns(stream); - return httpResponse; - } - - private static Mock GetMockFormatter() - { - var mockFormatter = new Mock(); - mockFormatter.Setup(o => o.CanWriteResult(It.IsAny(), - It.IsAny())) - .Returns(true); - - mockFormatter.Setup(o => o.WriteAsync(It.IsAny())) - .Returns(Task.FromResult(true)) - .Verifiable(); - return mockFormatter; - } - - private static IServiceProvider GetServiceProvider() - { - var options = new MvcOptions(); - var optionsAccessor = new Mock>(); - optionsAccessor.SetupGet(o => o.Value).Returns(options); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddInstance(optionsAccessor.Object); - return serviceCollection.BuildServiceProvider(); - } - - public class CannotWriteFormatter : IOutputFormatter - { - public virtual bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) - { - return false; - } - - public virtual Task WriteAsync(OutputFormatterContext context) - { - throw new NotImplementedException(); - } - } - - public class Person - { - public string Name { get; set; } } } } \ No newline at end of file