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:
parent
225b4903cf
commit
307c191c17
17
Mvc.sln
17
Mvc.sln
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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-*",
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue