Adding support for content negotiation.

This change consists of :
1. Conneg based on request headers, supports the following 3 scenarios:
	a. ContentType property on ObjectResult set to null or is empty.
	b. ContentType property on ObjectResult set to a single content type.
	c. ContentType property on ObjectResult set to multiple content types.

2. Parsing Helpers, comparers and extensions for comparing various http headers.
3. Tests.

Open workitems:
1. Remodel JsonResult and ContentResult to be a derivation of ObjectResult.
2. Populate DeclaredType.

Conflicts:
	src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterDescriptor.cs
	src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
	src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/OutputFormatterDescriptorExtensions.cs
	src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
	src/Microsoft.AspNet.Mvc.Core/Resources.resx
	src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeHeaderValue.cs
	src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeWithQualityHeaderValue.cs
	src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
	src/Microsoft.AspNet.Mvc/MvcServices.cs
	test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
	test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorExtensionTest.cs
	test/Microsoft.AspNet.Mvc.HeaderValueAbstractions.Test/MediaTypeHeaderValueParsingTests.cs
	test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
This commit is contained in:
harshgMSFT 2014-07-14 11:07:18 -07:00
parent 225b4903cf
commit 307c191c17
24 changed files with 1279 additions and 130 deletions

17
Mvc.sln
View File

@ -1,8 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.21901.1
VisualStudioVersion = 14.0.21806.0
VisualStudioVersion = 14.0.21916.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -57,10 +56,13 @@ EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatterWebSite", "test\WebSites\FormatterWebSite\FormatterWebSite.kproj", "{62735776-46FF-4170-9392-02E128A69B89}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ValueProvidersSite", "test\WebSites\ValueProvidersSite\ValueProvidersSite.kproj", "{14F79E79-AE79-48FA-95DE-D794EF4EABB3}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.HeaderValueAbstractions", "src\Microsoft.AspNet.Mvc.HeaderValueAbstractions\Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj", "{98335B23-E4B9-4CAD-9749-0DED32A659A1}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests", "test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests.kproj", "{E69FD235-2042-43A4-9970-59CB29955B4E}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ConnegWebsite", "test\WebSites\ConnegWebSite\ConnegWebsite.kproj", "{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -321,6 +323,16 @@ Global
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|x86.ActiveCfg = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Any CPU.Build.0 = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -352,5 +364,6 @@ Global
{14F79E79-AE79-48FA-95DE-D794EF4EABB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{98335B23-E4B9-4CAD-9749-0DED32A659A1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{E69FD235-2042-43A4-9970-59CB29955B4E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal

View File

@ -1,7 +1,14 @@
// 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.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
{
@ -9,29 +16,187 @@ namespace Microsoft.AspNet.Mvc
{
public object Value { get; set; }
public List<IOutputFormatter> Formatters { get; set; }
public List<MediaTypeHeaderValue> ContentTypes { get; set; }
public Type DeclaredType { get; set; }
public ObjectResult(object value)
{
Value = value;
Formatters = new List<IOutputFormatter>();
ContentTypes = new List<MediaTypeHeaderValue>();
}
public override async Task ExecuteResultAsync(ActionContext context)
{
ActionResult result;
var actionReturnString = Value as string;
if (actionReturnString != null)
var formatters = GetDefaultFormatters(context);
var formatterContext = new OutputFormatterContext()
{
result = new ContentResult
DeclaredType = DeclaredType,
ActionContext = context,
Object = Value,
};
var selectedFormatter = SelectFormatter(formatterContext, formatters);
if (selectedFormatter == null)
{
// No formatter supports this.
context.HttpContext.Response.StatusCode = 406;
return;
}
await selectedFormatter.WriteAsync(formatterContext);
}
public virtual IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters)
{
var incomingAcceptHeader = HeaderParsingHelpers.GetAcceptHeaders(
formatterContext.ActionContext.HttpContext.Request.Accept);
var sortedAcceptHeaders = SortMediaTypeWithQualityHeaderValues(incomingAcceptHeader)
.Where(header => header.Quality != FormattingUtilities.NoMatch)
.ToArray();
IOutputFormatter selectedFormatter = null;
if (ContentTypes == null || ContentTypes.Count == 0)
{
// Select based on sorted accept headers.
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
sortedAcceptHeaders);
if (selectedFormatter == null)
{
ContentType = "text/plain",
Content = actionReturnString,
};
// No formatter found based on accept headers, fall back on request contentType.
var incomingContentType =
MediaTypeHeaderValue.Parse(formatterContext.ActionContext.HttpContext.Request.ContentType);
// 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);
}
}
else if (ContentTypes.Count == 1)
{
// There is only one value that can be supported.
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
formatterContext,
formatters,
ContentTypes);
}
else
{
result = new JsonResult(Value);
// Filter and remove accept headers which cannot support any of the user specified content types.
var filteredAndSortedAcceptHeaders = sortedAcceptHeaders
.Where(acceptHeader =>
ContentTypes
.Any(contentType =>
contentType.IsSubsetOf(acceptHeader)))
.ToArray();
if (filteredAndSortedAcceptHeaders.Length > 0)
{
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);
}
}
await result.ExecuteResultAsync(context);
return selectedFormatter;
}
public virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> sortedAcceptHeaders)
{
IOutputFormatter selectedFormatter = null;
foreach (var contentType in sortedAcceptHeaders)
{
// Loop through each of the formatters and see if any one will support this
// mediaType Value.
selectedFormatter = formatters.FirstOrDefault(
formatter =>
formatter.CanWriteResult(formatterContext, contentType));
if (selectedFormatter != null)
{
// we found our match.
break;
}
}
return selectedFormatter;
}
public virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> formatters,
IEnumerable<MediaTypeHeaderValue> acceptableContentTypes)
{
var selectedFormatter = formatters.FirstOrDefault(
formatter =>
acceptableContentTypes
.Any(contentType =>
formatter.CanWriteResult(formatterContext, contentType)));
return selectedFormatter;
}
private static MediaTypeWithQualityHeaderValue[] SortMediaTypeWithQualityHeaderValues
(IEnumerable<MediaTypeWithQualityHeaderValue> headerValues)
{
if (headerValues == null)
{
return new MediaTypeWithQualityHeaderValue[] { };
}
// 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,
MediaTypeWithQualityHeaderValueComparer.QualityComparer)
.ToArray();
}
private IEnumerable<IOutputFormatter> GetDefaultFormatters(ActionContext context)
{
IEnumerable<IOutputFormatter> formatters = null;
if (Formatters == null || Formatters.Count == 0)
{
formatters = context.HttpContext
.RequestServices
.GetService<IOutputFormattersProvider>()
.OutputFormatters;
}
else
{
formatters = Formatters;
}
return formatters;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
{
/// <inheritdoc />
public class DefaultOutputFormattersProvider : IOutputFormattersProvider
{
private readonly List<OutputFormatterDescriptor> _descriptors;
private readonly ITypeActivator _typeActivator;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the DefaultOutputFormattersProvider class.
/// </summary>
/// <param name="options">An accessor to the <see cref="MvcOptions"/> configured for this application.</param>
/// <param name="typeActivator">An <see cref="ITypeActivator"/> instance used to instantiate types.</param>
/// <param name="serviceProvider">A <see cref="IServiceProvider"/> instance that retrieves services from the
/// service collection.</param>
public DefaultOutputFormattersProvider(IOptionsAccessor<MvcOptions> options,
ITypeActivator typeActivator,
IServiceProvider serviceProvider)
{
_descriptors = options.Options.OutputFormatters;
_typeActivator = typeActivator;
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
public IReadOnlyList<IOutputFormatter> OutputFormatters
{
get
{
var outputFormatters = new List<IOutputFormatter>();
foreach (var descriptor in _descriptors)
{
var formatter = descriptor.OutputFormatter;
if (formatter == null)
{
formatter = (IOutputFormatter)_typeActivator.CreateInstance(_serviceProvider,
descriptor.OutputFormatterType);
}
outputFormatters.Add(formatter);
}
return outputFormatters;
}
}
}
}

View File

@ -0,0 +1,43 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
public static class HeaderParsingHelpers
{
public static IList<MediaTypeWithQualityHeaderValue> GetAcceptHeaders(string acceptHeader)
{
if (string.IsNullOrEmpty(acceptHeader))
{
return null;
}
var acceptHeaderCollection = new List<MediaTypeWithQualityHeaderValue>();
foreach (var item in acceptHeader.Split(','))
{
acceptHeaderCollection.Add(MediaTypeWithQualityHeaderValue.Parse(item));
}
return acceptHeaderCollection;
}
public static IList<StringWithQualityHeaderValue> GetAcceptCharsetHeaders(string acceptCharsetHeader)
{
if (string.IsNullOrEmpty(acceptCharsetHeader))
{
return null;
}
var acceptCharsetHeaderCollection = new List<StringWithQualityHeaderValue>();
foreach (var item in acceptCharsetHeader.Split(','))
{
acceptCharsetHeaderCollection.Add(StringWithQualityHeaderValue.Parse(item));
}
return acceptCharsetHeaderCollection;
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Writes an object to the output stream.
/// </summary>
public interface IOutputFormatter
{
/// <summary>
/// Determines whether this <see cref="IOutputFormatter"/> can serialize
/// an object of the specified type.
/// </summary>
/// <param name="context">The formatter context associated with the call.</param>
/// <param name="contentType">The desired contentType on the response.</param>
/// <returns>True if this <see cref="IOutputFormatter"/> supports the passed in
/// <paramref name="contentType"/> and is able to serialize the object
/// represent by <paramref name="context"/>'s Object property.
/// False otherwise.</returns>
bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType);
/// <summary>
/// Writes the object represented by <paramref name="context"/>'s Object property.
/// </summary>
/// <param name="context">The formatter context associated with the call.</param>
/// <returns>A Task that serializes the value to the <paramref name="context"/>'s response message.</returns>
Task WriteAsync(OutputFormatterContext context);
}
}

View File

@ -0,0 +1,18 @@
// 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.Collections.Generic;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Provides an activated collection of <see cref="IOutputFormatter"/> instances.
/// </summary>
public interface IOutputFormattersProvider
{
/// <summary>
/// Gets a collection of activated OutputFormatter instances.
/// </summary>
IReadOnlyList<IOutputFormatter> OutputFormatters { get; }
}
}

View File

@ -73,28 +73,16 @@ namespace Microsoft.AspNet.Mvc
return jsonSerializer;
}
public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
{
return SupportedMediaTypes.Any(supportedMediaType =>
contentType.RawValue.Equals(supportedMediaType.RawValue,
StringComparison.OrdinalIgnoreCase));
}
public override Task WriteAsync(OutputFormatterContext context,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var response = context.HttpContext.Response;
// The content type including the encoding should have been set already.
// In case it was not present, a default will be selected.
var selectedEncoding = SelectCharacterEncoding(MediaTypeHeaderValue.Parse(response.ContentType));
using (var writer = new StreamWriter(response.Body, selectedEncoding))
var response = context.ActionContext.HttpContext.Response;
var selectedEncoding = context.SelectedEncoding;
using (var writer = new StreamWriter(response.Body, selectedEncoding, 1024, leaveOpen: true))
{
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, context.ObjectResult.Value);
jsonSerializer.Serialize(jsonWriter, context.Object);
// We're explicitly calling flush here to simplify the debugging experience because the
// underlying TextWriter might be long-lived. If this method ends up being called repeatedly

View File

@ -0,0 +1,105 @@
// 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.Generic;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Implementation of <see cref="IComparer{T}"/> that can compare accept media type header fields
/// based on their quality values (a.k.a q-values).
/// </summary>
public class MediaTypeWithQualityHeaderValueComparer : IComparer<MediaTypeWithQualityHeaderValue>
{
private static readonly MediaTypeWithQualityHeaderValueComparer _mediaTypeComparer =
new MediaTypeWithQualityHeaderValueComparer();
private MediaTypeWithQualityHeaderValueComparer()
{
}
public static MediaTypeWithQualityHeaderValueComparer QualityComparer
{
get { return _mediaTypeComparer; }
}
/// <summary>
/// Compares two <see cref="MediaTypeWithQualityHeaderValue"/> based on their quality value
/// (a.k.a their "q-value"). Values with identical q-values are considered equal (i.e the result is 0)
/// with the exception that sub-type wild-cards are considered less than specific media types and full
/// wild-cards are considered less than sub-type wild-cards. This allows to sort a sequence of
/// <see cref="MediaTypeWithQualityHeaderValue"/> following their q-values in the order of specific
/// media types, sub-type wildcards, and last any full wild-cards.
/// </summary>
/// <param name="mediaType1">The first <see cref="MediaTypeWithQualityHeaderValue"/> to compare.</param>
/// <param name="mediaType2">The second <see cref="MediaTypeWithQualityHeaderValue"/> to compare.</param>
/// <returns></returns>
public int Compare(MediaTypeWithQualityHeaderValue mediaType1, MediaTypeWithQualityHeaderValue mediaType2)
{
if (object.ReferenceEquals(mediaType1, mediaType2))
{
return 0;
}
var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2);
if (returnValue == 0)
{
if (!mediaType1.MediaType.Equals(mediaType2.MediaType, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MediaTypeRange == MediaTypeHeaderValueRange.AllMediaRange)
{
return -1;
}
else if (mediaType2.MediaTypeRange == MediaTypeHeaderValueRange.AllMediaRange)
{
return 1;
}
else if (mediaType1.MediaTypeRange == MediaTypeHeaderValueRange.SubtypeMediaRange &&
mediaType2.MediaTypeRange != MediaTypeHeaderValueRange.SubtypeMediaRange)
{
return -1;
}
else if (mediaType1.MediaTypeRange != MediaTypeHeaderValueRange.SubtypeMediaRange &&
mediaType2.MediaTypeRange == MediaTypeHeaderValueRange.SubtypeMediaRange)
{
return 1;
}
}
else if (!mediaType1.MediaSubType.Equals(mediaType2.MediaSubType, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MediaTypeRange == MediaTypeHeaderValueRange.SubtypeMediaRange)
{
return -1;
}
else if (mediaType2.MediaTypeRange == MediaTypeHeaderValueRange.SubtypeMediaRange)
{
return 1;
}
}
}
return returnValue;
}
private static int CompareBasedOnQualityFactor(MediaTypeWithQualityHeaderValue mediaType1,
MediaTypeWithQualityHeaderValue mediaType2)
{
var mediaType1Quality = mediaType1.Quality ?? FormattingUtilities.Match;
var mediaType2Quality = mediaType2.Quality ?? FormattingUtilities.Match;
var qualityDifference = mediaType1Quality - mediaType2Quality;
if (qualityDifference < 0)
{
return -1;
}
else if (qualityDifference > 0)
{
return 1;
}
return 0;
}
}
}

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
@ -16,20 +15,20 @@ namespace Microsoft.AspNet.Mvc
/// <summary>
/// Writes an object to the output stream.
/// </summary>
public abstract class OutputFormatter
public abstract class OutputFormatter : IOutputFormatter
{
/// <summary>
/// Gets the mutable collection of character encodings supported by
/// this <see cref="OutputFormatter"/> instance. The encodings are
/// used when writing the data.
/// </summary>
public List<Encoding> SupportedEncodings { get; private set; }
public IList<Encoding> SupportedEncodings { get; private set; }
/// <summary>
/// Gets the mutable collection of <see cref="MediaTypeHeaderValue"/> elements supported by
/// this <see cref="OutputFormatter"/> instance.
/// </summary>
public List<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
public IList<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="OutputFormatter"/> class.
@ -44,60 +43,133 @@ namespace Microsoft.AspNet.Mvc
/// Determines the best <see cref="Encoding"/> amongst the supported encodings
/// for reading or writing an HTTP entity body based on the provided <paramref name="contentTypeHeader"/>.
/// </summary>
/// <param name="contentTypeHeader">The content type header provided as part of the request or response.</param>
/// <param name="context">The formatter context associated with the call.
/// </param>
/// <returns>The <see cref="Encoding"/> to use when reading the request or writing the response.</returns>
public virtual Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentTypeHeader)
public virtual Encoding SelectCharacterEncoding(OutputFormatterContext context)
{
Encoding encoding = null;
if (contentTypeHeader != null)
var request = context.ActionContext.HttpContext.Request;
var encoding = MatchAcceptCharacterEncoding(request.AcceptCharset);
if (encoding == null)
{
// Find encoding based on content type charset parameter
var charset = contentTypeHeader.Charset;
if (!String.IsNullOrWhiteSpace(charset))
// Match based on request acceptHeader.
var requestContentType = MediaTypeHeaderValue.Parse(request.ContentType);
if (requestContentType != null && !string.IsNullOrEmpty(requestContentType.Charset))
{
var requestCharset = requestContentType.Charset;
encoding = SupportedEncodings.FirstOrDefault(
supportedEncoding =>
charset.Equals(supportedEncoding.WebName,
StringComparison.OrdinalIgnoreCase));
supportedEncoding =>
requestCharset.Equals(supportedEncoding.WebName));
}
}
if (encoding == null)
encoding = encoding ?? SupportedEncodings.FirstOrDefault();
return encoding;
}
/// <inheritdoc />
public virtual bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
MediaTypeHeaderValue mediaType = null;
if (contentType == null)
{
// We didn't find a character encoding match based on the content headers.
// Instead we try getting the default character encoding.
if (SupportedEncodings.Count > 0)
{
encoding = SupportedEncodings[0];
}
// If the desired content type is set to null, the current formatter is free to choose the
// response media type.
mediaType = SupportedMediaTypes.FirstOrDefault();
}
else
{
// Since supportedMedia Type is going to be more specific check if supportedMediaType is a subset
// of the content type which is typically what we get on acceptHeader.
mediaType = SupportedMediaTypes
.FirstOrDefault(supportedMediaType => supportedMediaType.IsSubsetOf(contentType));
}
if (encoding == null)
if (mediaType != null)
{
context.SelectedContentType = mediaType;
return true;
}
return false;
}
/// <inheritdoc />
public async Task WriteAsync(OutputFormatterContext context)
{
WriteResponseContentHeaders(context);
await WriteResponseBodyAsync(context);
}
/// <summary>
/// Sets the content-type headers with charset value to the HttpResponse.
/// </summary>
/// <param name="context">The formatter context associated with the call.</param>
public virtual void WriteResponseContentHeaders(OutputFormatterContext context)
{
var selectedMediaType = context.SelectedContentType;
// If content type is not set then set it based on supported media types.
selectedMediaType = selectedMediaType ?? SupportedMediaTypes.FirstOrDefault();
if (selectedMediaType == null)
{
throw new InvalidOperationException(Resources.FormatOutputFormatterNoMediaType(GetType().FullName));
}
var selectedEncoding = SelectCharacterEncoding(context);
if (selectedEncoding == null)
{
// No supported encoding was found so there is no way for us to start writing.
throw new InvalidOperationException(Resources.FormatOutputFormatterNoEncoding(GetType().FullName));
}
return encoding;
context.SelectedEncoding = selectedEncoding;
// Override the content type value even if one already existed.
selectedMediaType.Charset = selectedEncoding.WebName;
var response = context.ActionContext.HttpContext.Response;
response.ContentType = selectedMediaType.RawValue;
}
/// <summary>
/// Determines whether this <see cref="OutputFormatter"/> can serialize
/// an object of the specified type.
/// Writes the response body.
/// </summary>
/// <param name="context">The formatter context associated with the call</param>
/// <param name="contentType">The desired contentType on the response.</param>
/// <returns>True if this <see cref="OutputFormatter"/> is able to serialize the object
/// represent by <paramref name="context"/>'s ObjectResult and supports the passed in
/// <paramref name="contentType"/>.
/// False otherwise.</returns>
public abstract bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType);
/// <param name="context">The formatter context associated with the call.</param>
/// <returns>A task which can write the response body.</returns>
public abstract Task WriteResponseBodyAsync(OutputFormatterContext context);
/// <summary>
/// Writes given <paramref name="value"/> to the HttpResponse <paramref name="response"/> body stream.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A Task that serializes the value to the <paramref name="context"/>'s response message.</returns>
public abstract Task WriteAsync(OutputFormatterContext context, CancellationToken cancellationToken);
private Encoding MatchAcceptCharacterEncoding(string acceptCharsetHeader)
{
var acceptCharsetHeaders = HeaderParsingHelpers
.GetAcceptCharsetHeaders(acceptCharsetHeader);
if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0)
{
var sortedAcceptCharsetHeaders = acceptCharsetHeaders
.Where(acceptCharset =>
acceptCharset.Quality != FormattingUtilities.NoMatch)
.OrderByDescending(
m => m, StringWithQualityHeaderValueComparer.QualityComparer);
foreach (var acceptCharset in sortedAcceptCharsetHeaders)
{
var charset = acceptCharset.Value;
if (!string.IsNullOrWhiteSpace(charset))
{
var encoding = SupportedEncodings.FirstOrDefault(
supportedEncoding =>
charset.Equals(supportedEncoding.WebName,
StringComparison.OrdinalIgnoreCase) ||
charset.Equals("*", StringComparison.OrdinalIgnoreCase));
if (encoding != null)
{
return encoding;
}
}
}
}
return null;
}
}
}

View File

@ -2,16 +2,40 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Http;
using System.Text;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Represents information used by a formatter for participating in
/// output content negotiation and in writing out the response.
/// </summary>
public class OutputFormatterContext
{
public ObjectResult ObjectResult { get; set; }
/// <summary>
/// The return value of the action method.
/// </summary>
public object Object { get; set; }
/// <summary>
/// The declared return type of the action.
/// </summary>
public Type DeclaredType { get; set; }
public HttpContext HttpContext { get; set; }
/// <summary>
/// Action context associated with the current call.
/// </summary>
public ActionContext ActionContext { get; set; }
/// <summary>
/// The encoding which is chosen by the selected formatter.
/// </summary>
public Encoding SelectedEncoding { get; set; }
/// <summary>
/// The content type which is chosen by the selected formatter.
/// </summary>
public MediaTypeHeaderValue SelectedContentType { get; set; }
}
}

View File

@ -0,0 +1,72 @@
// 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.Generic;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Implementation of <see cref="IComparer{T}"/> that can compare content negotiation header fields
/// based on their quality values (a.k.a q-values). This applies to values used in accept-charset,
/// accept-encoding, accept-language and related header fields with similar syntax rules. See
/// <see cref="MediaTypeWithQualityHeaderValueComparer"/> for a comparer for media type
/// q-values.
/// </summary>
internal class StringWithQualityHeaderValueComparer : IComparer<StringWithQualityHeaderValue>
{
private static readonly StringWithQualityHeaderValueComparer _qualityComparer =
new StringWithQualityHeaderValueComparer();
private StringWithQualityHeaderValueComparer()
{
}
public static StringWithQualityHeaderValueComparer QualityComparer
{
get { return _qualityComparer; }
}
/// <summary>
/// Compares two <see cref="StringWithQualityHeaderValue"/> based on their quality value
/// (a.k.a their "q-value").
/// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card
/// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort
/// a sequence of <see cref="StringWithQualityHeaderValue"/> following their q-values ending up with any
/// wild-cards at the end.
/// </summary>
/// <param name="stringWithQuality1">The first value to compare.</param>
/// <param name="stringWithQuality2">The second value to compare</param>
/// <returns>The result of the comparison.</returns>
public int Compare([NotNull] StringWithQualityHeaderValue stringWithQuality1,
[NotNull] StringWithQualityHeaderValue stringWithQuality2)
{
var quality1 = stringWithQuality1.Quality ?? FormattingUtilities.Match;
var quality2 = stringWithQuality2.Quality ?? FormattingUtilities.Match;
var qualityDifference = quality1 - quality2;
if (qualityDifference < 0)
{
return -1;
}
else if (qualityDifference > 0)
{
return 1;
}
if (!String.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase))
{
if (String.Equals(stringWithQuality1.Value, "*", StringComparison.OrdinalIgnoreCase))
{
return -1;
}
else if (String.Equals(stringWithQuality2.Value, "*", StringComparison.OrdinalIgnoreCase))
{
return 1;
}
}
return 0;
}
}
}

View File

@ -44,9 +44,15 @@
<Compile Include="OptionDescriptors\ValueProviderFactoryDescriptorExtensions.cs" />
<Compile Include="OptionDescriptors\ViewEngineDescriptorExtensions.cs" />
<Compile Include="ExpiringFileInfoCache.cs" />
<Compile Include="Formatters\DefaultOutputFormattersProvider.cs" />
<Compile Include="Formatters\IOutputFormatter.cs" />
<Compile Include="Formatters\StringWithQualityHeaderValueComparer.cs" />
<Compile Include="IExpiringFileInfoCache.cs" />
<Compile Include="Formatters\IOutputFormattersProvider.cs" />
<Compile Include="Formatters\OutputFormatterContext.cs" />
<Compile Include="Formatters\HeaderParsingHelpers.cs" />
<Compile Include="Formatters\JsonOutputFormatter.cs" />
<Compile Include="Formatters\MediaTypeWithQualityHeaderValueComparer.cs" />
<Compile Include="Formatters\OutputFormatter.cs" />
<Compile Include="ParameterBinding\ModelBindingHelper.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />

View File

@ -13,10 +13,10 @@ namespace Microsoft.AspNet.Mvc
public static class OutputFormatterDescriptorExtensions
{
/// <summary>
/// Adds a type representing a <see cref="OutputFormatter"/> to a descriptor collection.
/// Adds a type representing a <see cref="IOutputFormatter"/> to a descriptor collection.
/// </summary>
/// <param name="descriptors">A list of OutputFormatterDescriptors</param>
/// <param name="outputFormatterType">Type representing an <see cref="OutputFormatter"/>.</param>
/// <param name="outputFormatterType">Type representing an <see cref="IOutputFormatter"/>.</param>
/// <returns>OutputFormatterDescriptor representing the added instance.</returns>
public static OutputFormatterDescriptor Add([NotNull] this IList<OutputFormatterDescriptor> descriptors,
[NotNull] Type outputFormatterType)
@ -27,10 +27,10 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Inserts a type representing a <see cref="OutputFormatter"/> to a descriptor collection.
/// Inserts a type representing a <see cref="IOutputFormatter"/> to a descriptor collection.
/// </summary>
/// <param name="descriptors">A list of OutputFormatterDescriptors</param>
/// <param name="outputFormatterType">Type representing an <see cref="OutputFormatter"/>.</param>
/// <param name="outputFormatterType">Type representing an <see cref="IOutputFormatter"/>.</param>
/// <returns>OutputFormatterDescriptor representing the inserted instance.</returns>
public static OutputFormatterDescriptor Insert([NotNull] this IList<OutputFormatterDescriptor> descriptors,
int index,
@ -47,13 +47,13 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Adds an <see cref="OutputFormatter"/> to a descriptor collection.
/// Adds an <see cref="IOutputFormatter"/> to a descriptor collection.
/// </summary>
/// <param name="descriptors">A list of OutputFormatterDescriptors</param>
/// <param name="outputFormatter">An <see cref="OutputFormatter"/> instance.</param>
/// <param name="outputFormatter">An <see cref="IOutputFormatter"/> instance.</param>
/// <returns>OutputFormatterDescriptor representing the added instance.</returns>
public static OutputFormatterDescriptor Add([NotNull] this IList<OutputFormatterDescriptor> descriptors,
[NotNull] OutputFormatter outputFormatter)
[NotNull] IOutputFormatter outputFormatter)
{
var descriptor = new OutputFormatterDescriptor(outputFormatter);
descriptors.Add(descriptor);
@ -61,14 +61,14 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Insert an <see cref="OutputFormatter"/> to a descriptor collection.
/// Insert an <see cref="IOutputFormatter"/> to a descriptor collection.
/// </summary>
/// <param name="descriptors">A list of OutputFormatterDescriptors</param>
/// <param name="outputFormatter">An <see cref="OutputFormatter"/> instance.</param>
/// <param name="outputFormatter">An <see cref="IOutputFormatter"/> instance.</param>
/// <returns>OutputFormatterDescriptor representing the added instance.</returns>
public static OutputFormatterDescriptor Insert([NotNull] this IList<OutputFormatterDescriptor> descriptors,
int index,
[NotNull] OutputFormatter outputFormatter)
[NotNull] IOutputFormatter outputFormatter)
{
if (index < 0 || index > descriptors.Count)
{

View File

@ -315,11 +315,15 @@
<data name="OutputFormatterNoEncoding" xml:space="preserve">
<value>No encoding found for output formatter '{0}'. There must be at least one supported encoding registered in order for the output formatter to write content.</value>
</data>
<data name="OutputFormatterNoMediaType" xml:space="preserve">
<value>No supported media type registered for output formatter '{0}'. There must be at least one supported media type registered in order for the output formatter to write content.</value>
</data>
<data name="AttributeRoute_AggregateErrorMessage" xml:space="preserve">
<value>The following errors occurred with attribute routing information:{0}{0}{1}</value>
<comment>{0} is the newline. {1} is the formatted list of errors using AttributeRoute_IndividualErrorMessage</comment>
</data>
<data name="AttributeRoute_CannotContainParameter" xml:space="preserve">
<data>
<value>The attribute route '{0}' cannot contain a parameter named '{{{1}}}'. Use '[{1}]' in the route template to insert the value '{2}'.</value>
</data>
<data name="AttributeRoute_IndividualErrorMessage" xml:space="preserve">

View File

@ -32,14 +32,13 @@ namespace Microsoft.AspNet.Mvc
options.ModelBinders.Add(new MutableObjectModelBinder());
options.ModelBinders.Add(new ComplexModelDtoModelBinder());
// Set up default output formatters.
options.OutputFormatters.Add(new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true));
// Set up ValueProviders
options.ValueProviderFactories.Add(new RouteValueValueProviderFactory());
options.ValueProviderFactories.Add(new RouteValueValueProviderFactory());
options.ValueProviderFactories.Add(new QueryStringValueProviderFactory());
options.ValueProviderFactories.Add(new FormValueProviderFactory());
// Set up OutputFormatters
options.OutputFormatters.Add(
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false));
}
}
}

View File

@ -66,10 +66,9 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IInputFormatter, XmlDataContractSerializerInputFormatter>();
yield return describe.Transient<IInputFormatterProvider, TempInputFormatterProvider>();
yield return describe.Transient<IModelBinderProvider, DefaultModelBindersProvider>();
yield return describe.Scoped<ICompositeModelBinder, CompositeModelBinder>();
yield return describe.Transient<IValueProviderFactoryProvider, DefaultValueProviderFactoryProvider>();
yield return describe.Scoped<ICompositeValueProviderFactory, CompositeValueProviderFactory>();
yield return describe.Transient<IOutputFormattersProvider, DefaultOutputFormattersProvider>();
yield return describe.Transient<IModelBindersProvider, DefaultModelBindersProvider>();
yield return describe.Transient<ICompositeModelBinder, CompositeModelBinder>();
yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>();

View File

@ -1,21 +1,97 @@
// 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.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
{
public class ObjectContentResultTests
public class ObjectResultTests
{
public static IEnumerable<object[]> 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("ContentTypes")]
public async Task ObjectResult_WithMultipleContentTypesAndAcceptHeaders_PerformsContentNegotiation(
IEnumerable<string> contentTypes, string acceptHeader, string expectedHeader)
{
// Arrange
var expectedContentType = expectedHeader;
var input = "testInput";
var stream = new MemoryStream();
var httpResponse = new Mock<HttpResponse>();
var tempContentType = string.Empty;
httpResponse.SetupProperty<string>(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<IOutputFormatter>
{
new CannotWriteFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
};
// 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 ObjectContentResult_Create_CallsContentResult_InitializesValue()
public void ObjectResult_Create_CallsContentResult_InitializesValue()
{
// Arrange
var input = "testInput";
@ -29,7 +105,212 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
}
[Fact]
public async Task ObjectContentResult_Execute_CallsContentResult_SetsContent()
public async Task ObjectResult_WithSingleContentType_TheGivenContentTypeIsSelected()
{
// Arrange
var expectedContentType = "application/json;charset=utf-8";
var input = "testInput";
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<MediaTypeHeaderValue>();
result.ContentTypes.Add(MediaTypeHeaderValue.Parse(expectedContentType));
// 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<IOutputFormatter>
{
new CannotWriteFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
};
// 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<IOutputFormatter>
{
mockFormatter.Object,
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
new CannotWriteFormatter()
};
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
// Verify that mock formatter was chosen.
mockFormatter.Verify(o => o.WriteAsync(It.IsAny<OutputFormatterContext>()));
}
[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<IOutputFormatter>
{
new CannotWriteFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
};
// 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>();
httpResponse.SetupProperty<string>(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<IOutputFormatter>
{
new CannotWriteFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
};
// 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_NoContentTypeSetWithNoAcceptHeadersAndNoRequestContentType_PicksFirstFormatterWhichCanWrite()
{
// Arrange
var stream = new MemoryStream();
var expectedContentType = "application/json;charset=utf-8";
var httpResponse = new Mock<HttpResponse>();
httpResponse.SetupProperty<string>(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<IOutputFormatter>
{
new CannotWriteFormatter(),
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true),
};
// 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>();
httpResponse.SetupProperty<string>(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<IOutputFormatter>
{
new CannotWriteFormatter(),
};
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
// Asserts that content type is not text/custom.
httpResponse.VerifySet(r => r.StatusCode = 406);
}
// TODO: Disabling since this scenario is no longer dealt with in object result.
// Re-enable once we do.
//[Fact]
public async Task ObjectResult_Execute_CallsContentResult_SetsContent()
{
// Arrange
var expectedContentType = "text/plain";
@ -37,7 +318,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
var stream = new MemoryStream();
var httpResponse = new Mock<HttpResponse>();
httpResponse.SetupSet(r => r.ContentType = expectedContentType).Verifiable();
var tempContentType = string.Empty;
httpResponse.SetupProperty<string>(o => o.ContentType);
httpResponse.SetupGet(r => r.Body).Returns(stream);
var actionContext = CreateMockActionContext(httpResponse.Object);
@ -53,21 +335,34 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
}
[Fact]
public async Task ObjectContentResult_Execute_CallsJsonResult_SetsContent()
public async Task ObjectResult_Execute_CallsJsonResult_SetsContent()
{
// Arrange
var expectedContentType = "application/json";
var expectedContentType = "application/json;charset=utf-8";
var nonStringValue = new { x1 = 10, y1 = "Hello" };
var httpResponse = Mock.Of<HttpResponse>();
httpResponse.Body = new MemoryStream();
var actionContext = CreateMockActionContext(httpResponse);
var tempStream = new MemoryStream();
using (var writer = new StreamWriter(tempStream, Encodings.UTF8EncodingWithoutBOM, 1024, leaveOpen: true))
{
var formatter = new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), false);
formatter.WriteObject(writer, nonStringValue);
}
var tempHttpContext = new Mock<HttpContext>();
var tempHttpResponse = new Mock<HttpResponse>();
tempHttpResponse.SetupGet(o => o.Body).Returns(tempStream);
tempHttpResponse.SetupProperty<string>(o => o.ContentType);
tempHttpContext.SetupGet(o => o.Response).Returns(tempHttpResponse.Object);
tempHttpContext.SetupGet(o => o.Request.AcceptCharset).Returns(string.Empty);
var tempActionContext = new ActionContext(tempHttpContext.Object,
new RouteData(),
new ActionDescriptor());
var formatterContext = new OutputFormatterContext()
{
ActionContext = tempActionContext,
Object = nonStringValue,
DeclaredType = nonStringValue.GetType()
};
var formatter = new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), true);
formatter.WriteResponseContentHeaders(formatterContext);
await formatter.WriteAsync(formatterContext);
// Act
var result = new ObjectResult(nonStringValue);
@ -78,15 +373,107 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
Assert.Equal(tempStream.ToArray(), ((MemoryStream)actionContext.HttpContext.Response.Body).ToArray());
}
private static ActionContext CreateMockActionContext(HttpResponse response = null)
private static ActionContext CreateMockActionContext(HttpResponse response = null,
string requestAcceptHeader = "application/*",
string requestContentType = "application/json",
string requestAcceptCharsetHeader = "")
{
var httpContext = new Mock<HttpContext>();
if (response != null)
{
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 Mock<HttpRequest>();
request.SetupGet(r => r.AcceptCharset).Returns(requestAcceptCharsetHeader);
request.SetupGet(r => r.Accept).Returns(requestAcceptHeader);
request.SetupGet(r => r.ContentType).Returns(requestContentType);
request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes));
httpContext.Setup(o => o.Request).Returns(request.Object);
httpContext.Setup(o => o.RequestServices).Returns(GetServiceProvider());
httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider)))
.Returns(new TestOutputFormatterProvider());
return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
}
private static Mock<HttpResponse> GetMockHttpResponse()
{
var stream = new MemoryStream();
var httpResponse = new Mock<HttpResponse>();
httpResponse.SetupProperty<string>(o => o.ContentType);
httpResponse.SetupGet(r => r.Body).Returns(stream);
return httpResponse;
}
private static Mock<CannotWriteFormatter> GetMockFormatter()
{
var mockFormatter = new Mock<CannotWriteFormatter>();
mockFormatter.Setup(o => o.CanWriteResult(It.IsAny<OutputFormatterContext>(),
It.IsAny<MediaTypeHeaderValue>()))
.Returns(true);
mockFormatter.Setup(o => o.WriteAsync(It.IsAny<OutputFormatterContext>()))
.Returns(Task.FromResult<bool>(true))
.Verifiable();
return mockFormatter;
}
private static IServiceProvider GetServiceProvider()
{
var optionsSetup = new MvcOptionsSetup();
var options = new MvcOptions();
optionsSetup.Setup(options);
var optionsAccessor = new Mock<IOptionsAccessor<MvcOptions>>();
optionsAccessor.SetupGet(o => o.Options).Returns(options);
var serviceCollection = new ServiceCollection();
serviceCollection.AddInstance<IOptionsAccessor<MvcOptions>>(optionsAccessor.Object);
return serviceCollection.BuildServiceProvider();
}
public class CannotWriteFormatter : IOutputFormatter
{
public List<Encoding> SupportedEncodings
{
get
{
throw new NotImplementedException();
}
}
public List<MediaTypeHeaderValue> SupportedMediaTypes
{
get
{
throw new NotImplementedException();
}
}
public virtual bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
return false;
}
public virtual Task WriteAsync(OutputFormatterContext context)
{
throw new NotImplementedException();
}
}
private class TestOutputFormatterProvider : IOutputFormattersProvider
{
public IReadOnlyList<IOutputFormatter> OutputFormatters
{
get
{
return new List<IOutputFormatter>()
{ new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: true) };
}
}
}
}
}

View File

@ -2,41 +2,110 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Test
{
public class OutputFormatterTests
{
[Fact]
public void SelectCharacterEncoding_FormatterWithNoEncoding_Throws()
public static IEnumerable<object[]> SelectResponseCharacterEncodingData
{
get
{
// string acceptEncodings, string requestEncoding, string[] supportedEncodings, string expectedEncoding
yield return new object[] { "", null, new string[] { "utf-8", "utf-16" }, "utf-8" };
yield return new object[] { "", "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
yield return new object[] { "utf-8", null, new string[] { "utf-8", "utf-16" }, "utf-8" };
yield return new object[] { "utf-16", "utf-8", new string[] { "utf-8", "utf-16" }, "utf-16" };
yield return new object[] { "utf-16; q=0.5", "utf-8", new string[] { "utf-8", "utf-16" }, "utf-16" };
yield return new object[] { "utf-8; q=0.0", null, new string[] { "utf-8", "utf-16" }, "utf-8" };
yield return new object[] { "utf-8; q=0.0", "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
yield return new object[]
{ "utf-8; q=0.0, utf-16; q=0.0", "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
yield return new object[]
{ "utf-8; q=0.0, utf-16; q=0.0", null, new string[] { "utf-8", "utf-16" }, "utf-8" };
yield return new object[] { "*; q=0.0", null, new string[] { "utf-8", "utf-16" }, "utf-8" };
yield return new object[] { "*; q=0.0", "utf-16", new string[] { "utf-8", "utf-16" }, "utf-16" };
}
}
[Theory]
[MemberData("SelectResponseCharacterEncodingData")]
public void SelectResponseCharacterEncoding_SelectsEncoding(string acceptCharsetHeaders,
string requestEncoding,
string[] supportedEncodings,
string expectedEncoding)
{
// Arrange
var testFormatter = new TestFormatter();
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.SetupGet(o => o.Request.AcceptCharset)
.Returns(acceptCharsetHeaders);
mockHttpContext.SetupGet(o => o.Request.ContentType)
.Returns("application/acceptCharset;charset=" + requestEncoding);
var actionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor());
var formatter = new TestOutputFormatter();
foreach (string supportedEncoding in supportedEncodings)
{
formatter.SupportedEncodings.Add(Encoding.GetEncoding(supportedEncoding));
}
var formatterContext = new OutputFormatterContext()
{
Object = "someValue",
ActionContext = actionContext,
DeclaredType = typeof(string)
};
// Act
var actualEncoding = formatter.SelectCharacterEncoding(formatterContext);
// Assert
Assert.Equal(Encoding.GetEncoding(expectedEncoding), actualEncoding);
}
[Fact]
public void SetResponseContentHeaders_FormatterWithNoEncoding_Throws()
{
// Arrange
var testFormatter = new TestOutputFormatter();
var testContentType = MediaTypeHeaderValue.Parse("text/invalid");
var formatterContext = new OutputFormatterContext();
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.SetupGet(o => o.Request.AcceptCharset)
.Returns(string.Empty);
var actionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor());
formatterContext.ActionContext = actionContext;
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => testFormatter.SelectCharacterEncoding(testContentType));
Assert.Equal("No encoding found for output formatter "+
"'Microsoft.AspNet.Mvc.Test.OutputFormatterTests+TestFormatter'." +
var ex = Assert.Throws<InvalidOperationException>(
() => testFormatter.WriteResponseContentHeaders(formatterContext));
Assert.Equal("No encoding found for output formatter " +
"'Microsoft.AspNet.Mvc.Test.OutputFormatterTests+TestOutputFormatter'." +
" There must be at least one supported encoding registered in order for the" +
" output formatter to write content.", ex.Message);
}
private class TestFormatter : OutputFormatter
private class TestOutputFormatter : OutputFormatter
{
public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
public TestOutputFormatter()
{
throw new NotImplementedException();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/acceptCharset"));
}
public override Task WriteAsync(OutputFormatterContext context, CancellationToken cancellationToken)
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
{
throw new NotImplementedException();
return Task.FromResult(true);
}
}
}

View File

@ -0,0 +1,84 @@
// 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.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Test
{
public class MediaTypeWithQualityHeaderValueTests
{
public static IEnumerable<object[]> SortValues
{
get
{
yield return new object[] {
new string[]
{
"application/*",
"text/plain",
"text/plain;q=1.0",
"text/plain",
"text/plain;q=0",
"*/*;q=0.8",
"*/*;q=1",
"text/*;q=1",
"text/plain;q=0.8",
"text/*;q=0.8",
"text/*;q=0.6",
"text/*;q=1.0",
"*/*;q=0.4",
"text/plain;q=0.6",
"text/xml",
},
new string[]
{
"text/plain",
"text/plain;q=1.0",
"text/plain",
"text/xml",
"application/*",
"text/*;q=1",
"text/*;q=1.0",
"*/*;q=1",
"text/plain;q=0.8",
"text/*;q=0.8",
"*/*;q=0.8",
"text/plain;q=0.6",
"text/*;q=0.6",
"*/*;q=0.4",
"text/plain;q=0",
}
};
}
}
[Theory]
[MemberData("SortValues")]
public void SortMediaTypeWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
{
// Arrange
var unsortedValues =
new List<MediaTypeWithQualityHeaderValue>(unsorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u)));
var expectedSortedValues =
new List<MediaTypeWithQualityHeaderValue>(expectedSorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u)));
// Act
var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeWithQualityHeaderValueComparer.QualityComparer).ToArray();
// Assert
for (int i = 0; i < expectedSortedValues.Count; i++)
{
Assert.True(MediaTypeWithQualityHeaderValueComparer.QualityComparer.Compare(expectedSortedValues[i], actualSorted[i]) == 0);
}
}
}
}

View File

@ -46,6 +46,7 @@
<Compile Include="ExpiringFileInfoCacheTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsTests.cs" />
<Compile Include="Formatters\OutputFormatterTests.cs" />
<Compile Include="MediaTypeWithQualityHeaderValueTests.cs" />
<Compile Include="ParameterBinding\ModelBindingHelperTest.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />

View File

@ -1,9 +1,9 @@
// 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.
#if NET45
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.OptionDescriptors;
using Moq;
using Xunit;
@ -19,13 +19,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Arrange
var collection = new List<OutputFormatterDescriptor>
{
new OutputFormatterDescriptor(Mock.Of<OutputFormatter>()),
new OutputFormatterDescriptor(Mock.Of<OutputFormatter>())
new OutputFormatterDescriptor(Mock.Of<IOutputFormatter>()),
new OutputFormatterDescriptor(Mock.Of<IOutputFormatter>())
};
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>("index",
() => collection.Insert(index, typeof(OutputFormatter)));
Assert.Throws<ArgumentOutOfRangeException>("index",
() => collection.Insert(index, typeof(IOutputFormatter)));
}
[Theory]
@ -36,10 +36,10 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Arrange
var collection = new List<OutputFormatterDescriptor>
{
new OutputFormatterDescriptor(Mock.Of<OutputFormatter>()),
new OutputFormatterDescriptor(Mock.Of<OutputFormatter>())
new OutputFormatterDescriptor(Mock.Of<IOutputFormatter>()),
new OutputFormatterDescriptor(Mock.Of<IOutputFormatter>())
};
var formatter = Mock.Of<OutputFormatter>();
var formatter = Mock.Of<IOutputFormatter>();
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>("index", () => collection.Insert(index, formatter));
@ -49,8 +49,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public void OutputFormatterDescriptors_AddsTypesAndInstances()
{
// Arrange
var formatter1 = Mock.Of<OutputFormatter>();
var formatter2 = Mock.Of<OutputFormatter>();
var formatter1 = Mock.Of<IOutputFormatter>();
var formatter2 = Mock.Of<IOutputFormatter>();
var type1 = typeof(JsonOutputFormatter);
var type2 = typeof(OutputFormatter);
var collection = new List<OutputFormatterDescriptor>();
@ -63,10 +63,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Assert
Assert.Equal(4, collection.Count);
Assert.Equal(formatter1, collection[0].Instance);
Assert.Equal(formatter2, collection[1].Instance);
Assert.Equal(type2, collection[2].OptionType);
Assert.Equal(type1, collection[3].OptionType);
Assert.Equal(formatter1, collection[0].OutputFormatter);
Assert.Equal(formatter2, collection[1].OutputFormatter);
Assert.Equal(type2, collection[2].OutputFormatterType);
Assert.Equal(type1, collection[3].OutputFormatterType);
}
}
}
#endif

View File

@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.Core
{
// Arrange
var expected = "The type 'System.String' must derive from " +
"'Microsoft.AspNet.Mvc.OutputFormatter'.";
"'Microsoft.AspNet.Mvc.IOutputFormatter'.";
var type = typeof(string);

View File

@ -9,6 +9,7 @@
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",
"Microsoft.AspNet.TestHost": "1.0.0-*",
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": "",
"Microsoft.Framework.ConfigurationModel": "1.0.0-*",
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*",

View File

@ -24,6 +24,7 @@
</PropertyGroup>
<ItemGroup>
<Content Include="Project.json" />
<Content Include="TestFiles\Input\InjectWithModel.cshtml" />
<Content Include="TestFiles\Input\Model.cshtml" />
<Content Include="TestFiles\Input\Inject.cshtml" />
</ItemGroup>
@ -37,7 +38,6 @@
<Compile Include="SpanFactory\SpanFactoryExtensions.cs" />
<Compile Include="SpanFactory\UnclassifiedSpanConstructor.cs" />
<Compile Include="StringTextBuffer.cs" />
<Compile Include="TestFiles\Input\InjectWithModel.cshtml" />
<Compile Include="TestFiles\Output\InjectWithModel.cs" />
<Compile Include="TestFiles\Output\Model.cs" />
<Compile Include="TestFiles\Output\Inject.cs" />