// Copyright (c) Microsoft Open Technologies, Inc. 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; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Internal; using Microsoft.Framework.OptionsModel; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Mvc { public class ObjectResult : ActionResult { public ObjectResult(object value) { Value = value; Formatters = new List(); ContentTypes = new List(); } public object Value { get; set; } public IList Formatters { get; set; } public IList ContentTypes { get; set; } public Type DeclaredType { get; set; } /// /// Gets or sets the HTTP status code. /// public int? StatusCode { get; set; } public override async Task ExecuteResultAsync(ActionContext context) { // 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, ActionContext = context, Object = Value, StatusCode = StatusCode }; var selectedFormatter = SelectFormatter(formatterContext, formatters); if (selectedFormatter == null) { // No formatter supports this. context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable; return; } if (StatusCode.HasValue) { context.HttpContext.Response.StatusCode = StatusCode.Value; } OnFormatting(context); await selectedFormatter.WriteAsync(formatterContext); } public virtual IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext, IEnumerable formatters) { if (ContentTypes.Count == 1) { // There is only one content type specified so we can skip looking at the accept headers. return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, ContentTypes); } var incomingAcceptHeaderMediaTypes = formatterContext.ActionContext.HttpContext.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.ActionContext.HttpContext .RequestServices .GetRequiredService>() .Options; var respectAcceptHeader = true; if (options.RespectBrowserAcceptHeader == false && incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes)) { respectAcceptHeader = false; } IEnumerable sortedAcceptHeaderMediaTypes = null; if (respectAcceptHeader) { sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes) .Where(header => header.Quality != HeaderQuality.NoMatch); } IOutputFormatter selectedFormatter = null; if (ContentTypes == null || ContentTypes.Count == 0) { if (respectAcceptHeader) { // Select based on sorted accept headers. selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( formatterContext, formatters, sortedAcceptHeaderMediaTypes); } if (selectedFormatter == null) { var requestContentType = formatterContext.ActionContext.HttpContext.Request.ContentType; // No formatter found based on accept headers, fall back on request contentType. MediaTypeHeaderValue incomingContentType = null; MediaTypeHeaderValue.TryParse(requestContentType, out incomingContentType); // In case the incomingContentType is null (as can be the case with get requests), // we need to pick the first formatter which // can support writing this type. var contentTypes = new[] { incomingContentType }; selectedFormatter = SelectFormatterUsingAnyAcceptableContentType( formatterContext, formatters, contentTypes); // This would be the case when no formatter could write the type base on the // accept headers and the request content type. Fallback on type based match. if (selectedFormatter == null) { foreach (var formatter in formatters) { var supportedContentTypes = formatter.GetSupportedContentTypes( formatterContext.DeclaredType, formatterContext.Object?.GetType(), contentType: null); if (formatter.CanWriteResult(formatterContext, supportedContentTypes?.FirstOrDefault())) { return formatter; } } } } } else { if (respectAcceptHeader) { // Filter and remove accept headers which cannot support any of the user specified content types. 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 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 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) { formatters = context.HttpContext .RequestServices .GetRequiredService() .OutputFormatters; } else { formatters = Formatters; } return formatters; } /// /// This method is called before the formatter writes to the output stream. /// protected virtual void OnFormatting([NotNull] ActionContext context) { } } }