From 307c191c177d376004f3a7e045ee5f788c6c7423 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Mon, 14 Jul 2014 11:07:18 -0700 Subject: [PATCH] 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 --- Mvc.sln | 17 +- .../ActionResults/ObjectResult.cs | 183 +++++++- .../DefaultOutputFormattersProvider.cs | 58 +++ .../Formatters/HeaderParsingHelpers.cs | 43 ++ .../Formatters/IOutputFormatter.cs | 40 ++ .../Formatters/IOutputFormattersProvider.cs | 18 + .../Formatters/JsonOutputFormatter.cs | 22 +- ...MediaTypeWithQualityHeaderValueComparer.cs | 105 +++++ .../Formatters/OutputFormatter.cs | 148 +++++-- .../Formatters/OutputFormatterContext.cs | 30 +- .../StringWithQualityHeaderValueComparer.cs | 72 +++ .../Microsoft.AspNet.Mvc.Core.kproj | 6 + .../OutputFormatterDescriptorExtensions.cs | 20 +- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 6 +- src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 9 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 7 +- .../ActionResults/ObjectContentResultTests.cs | 415 +++++++++++++++++- .../Formatters/OutputFormatterTests.cs | 91 +++- .../MediaTypeWithQualityHeaderValueTests.cs | 84 ++++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + .../OutputFormatterDescriptorExtensionTest.cs | 29 +- .../OutputFormatterDescriptorTest.cs | 2 +- .../project.json | 1 + ...Microsoft.AspNet.Mvc.Razor.Host.Test.kproj | 2 +- 24 files changed, 1279 insertions(+), 130 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultOutputFormattersProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/HeaderParsingHelpers.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormattersProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeWithQualityHeaderValueComparer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Formatters/StringWithQualityHeaderValueComparer.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/MediaTypeWithQualityHeaderValueTests.cs diff --git a/Mvc.sln b/Mvc.sln index 636e42cb3c..3d30fd892c 100644 --- a/Mvc.sln +++ b/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 diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs index c22fa073ab..eafb61481e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs @@ -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 Formatters { get; set; } + + public List ContentTypes { get; set; } + + public Type DeclaredType { get; set; } + public ObjectResult(object value) { Value = value; + Formatters = new List(); + ContentTypes = new List(); } 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 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 formatters, + IEnumerable sortedAcceptHeaders) + { + IOutputFormatter selectedFormatter = null; + foreach (var contentType in sortedAcceptHeaders) + { + // Loop through each of the formatters and see if any one will support this + // mediaType Value. + selectedFormatter = formatters.FirstOrDefault( + formatter => + formatter.CanWriteResult(formatterContext, contentType)); + if (selectedFormatter != null) + { + // we found our match. + break; + } + } + + return selectedFormatter; + } + + public virtual IOutputFormatter SelectFormatterUsingAnyAcceptableContentType( + OutputFormatterContext formatterContext, + IEnumerable formatters, + IEnumerable acceptableContentTypes) + { + var selectedFormatter = formatters.FirstOrDefault( + formatter => + acceptableContentTypes + .Any(contentType => + formatter.CanWriteResult(formatterContext, contentType))); + return selectedFormatter; + } + + private static MediaTypeWithQualityHeaderValue[] SortMediaTypeWithQualityHeaderValues + (IEnumerable 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 GetDefaultFormatters(ActionContext context) + { + IEnumerable formatters = null; + if (Formatters == null || Formatters.Count == 0) + { + formatters = context.HttpContext + .RequestServices + .GetService() + .OutputFormatters; + } + else + { + formatters = Formatters; + } + + return formatters; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultOutputFormattersProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultOutputFormattersProvider.cs new file mode 100644 index 0000000000..578f9de02b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultOutputFormattersProvider.cs @@ -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 +{ + /// + public class DefaultOutputFormattersProvider : IOutputFormattersProvider + { + private readonly List _descriptors; + private readonly ITypeActivator _typeActivator; + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the DefaultOutputFormattersProvider class. + /// + /// An accessor to the configured for this application. + /// An instance used to instantiate types. + /// A instance that retrieves services from the + /// service collection. + public DefaultOutputFormattersProvider(IOptionsAccessor options, + ITypeActivator typeActivator, + IServiceProvider serviceProvider) + { + _descriptors = options.Options.OutputFormatters; + _typeActivator = typeActivator; + _serviceProvider = serviceProvider; + } + + /// + public IReadOnlyList OutputFormatters + { + get + { + var outputFormatters = new List(); + foreach (var descriptor in _descriptors) + { + var formatter = descriptor.OutputFormatter; + if (formatter == null) + { + formatter = (IOutputFormatter)_typeActivator.CreateInstance(_serviceProvider, + descriptor.OutputFormatterType); + } + + outputFormatters.Add(formatter); + } + + return outputFormatters; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HeaderParsingHelpers.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HeaderParsingHelpers.cs new file mode 100644 index 0000000000..551d940f2a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HeaderParsingHelpers.cs @@ -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 GetAcceptHeaders(string acceptHeader) + { + if (string.IsNullOrEmpty(acceptHeader)) + { + return null; + } + + var acceptHeaderCollection = new List(); + foreach (var item in acceptHeader.Split(',')) + { + acceptHeaderCollection.Add(MediaTypeWithQualityHeaderValue.Parse(item)); + } + + return acceptHeaderCollection; + } + + public static IList GetAcceptCharsetHeaders(string acceptCharsetHeader) + { + if (string.IsNullOrEmpty(acceptCharsetHeader)) + { + return null; + } + + var acceptCharsetHeaderCollection = new List(); + foreach (var item in acceptCharsetHeader.Split(',')) + { + acceptCharsetHeaderCollection.Add(StringWithQualityHeaderValue.Parse(item)); + } + + return acceptCharsetHeaderCollection; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs new file mode 100644 index 0000000000..a4eb3fbc12 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs @@ -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 +{ + /// + /// Writes an object to the output stream. + /// + public interface IOutputFormatter + { + /// + /// Determines whether this can serialize + /// an object of the specified type. + /// + /// The formatter context associated with the call. + /// The desired contentType on the response. + /// True if this supports the passed in + /// and is able to serialize the object + /// represent by 's Object property. + /// False otherwise. + bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType); + + /// + /// Writes the object represented by 's Object property. + /// + /// The formatter context associated with the call. + /// A Task that serializes the value to the 's response message. + Task WriteAsync(OutputFormatterContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormattersProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormattersProvider.cs new file mode 100644 index 0000000000..ef0ac4b848 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormattersProvider.cs @@ -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 +{ + /// + /// Provides an activated collection of instances. + /// + public interface IOutputFormattersProvider + { + /// + /// Gets a collection of activated OutputFormatter instances. + /// + IReadOnlyList OutputFormatters { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs index 80f443b5f5..df69d866dc 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs @@ -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 diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeWithQualityHeaderValueComparer.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000..163d56efaf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeWithQualityHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of that can compare accept media type header fields + /// based on their quality values (a.k.a q-values). + /// + public class MediaTypeWithQualityHeaderValueComparer : IComparer + { + private static readonly MediaTypeWithQualityHeaderValueComparer _mediaTypeComparer = + new MediaTypeWithQualityHeaderValueComparer(); + + private MediaTypeWithQualityHeaderValueComparer() + { + } + + public static MediaTypeWithQualityHeaderValueComparer QualityComparer + { + get { return _mediaTypeComparer; } + } + + /// + /// Compares two 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 + /// following their q-values in the order of specific + /// media types, sub-type wildcards, and last any full wild-cards. + /// + /// The first to compare. + /// The second to compare. + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs index 2a4c7bb891..f74f46f495 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -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 /// /// Writes an object to the output stream. /// - public abstract class OutputFormatter + public abstract class OutputFormatter : IOutputFormatter { /// /// Gets the mutable collection of character encodings supported by /// this instance. The encodings are /// used when writing the data. /// - public List SupportedEncodings { get; private set; } + public IList SupportedEncodings { get; private set; } /// /// Gets the mutable collection of elements supported by /// this instance. /// - public List SupportedMediaTypes { get; private set; } + public IList SupportedMediaTypes { get; private set; } /// /// Initializes a new instance of the class. @@ -44,60 +43,133 @@ namespace Microsoft.AspNet.Mvc /// Determines the best amongst the supported encodings /// for reading or writing an HTTP entity body based on the provided . /// - /// The content type header provided as part of the request or response. + /// The formatter context associated with the call. + /// /// The to use when reading the request or writing the response. - 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; + } + + /// + 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; + } + + /// + public async Task WriteAsync(OutputFormatterContext context) + { + WriteResponseContentHeaders(context); + await WriteResponseBodyAsync(context); + } + + /// + /// Sets the content-type headers with charset value to the HttpResponse. + /// + /// The formatter context associated with the call. + 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; } /// - /// Determines whether this can serialize - /// an object of the specified type. + /// Writes the response body. /// - /// The formatter context associated with the call - /// The desired contentType on the response. - /// True if this is able to serialize the object - /// represent by 's ObjectResult and supports the passed in - /// . - /// False otherwise. - public abstract bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType); + /// The formatter context associated with the call. + /// A task which can write the response body. + public abstract Task WriteResponseBodyAsync(OutputFormatterContext context); - /// - /// Writes given to the HttpResponse body stream. - /// - /// The token to monitor for cancellation requests. - /// A Task that serializes the value to the 's response message. - 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; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs index 217b908417..7ee3509a78 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatterContext.cs @@ -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 { + /// + /// Represents information used by a formatter for participating in + /// output content negotiation and in writing out the response. + /// public class OutputFormatterContext { - public ObjectResult ObjectResult { get; set; } + /// + /// The return value of the action method. + /// + public object Object { get; set; } + /// + /// The declared return type of the action. + /// public Type DeclaredType { get; set; } - public HttpContext HttpContext { get; set; } + /// + /// Action context associated with the current call. + /// + public ActionContext ActionContext { get; set; } + + /// + /// The encoding which is chosen by the selected formatter. + /// + public Encoding SelectedEncoding { get; set; } + + /// + /// The content type which is chosen by the selected formatter. + /// + public MediaTypeHeaderValue SelectedContentType { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/StringWithQualityHeaderValueComparer.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/StringWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000..1f39c3962e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/StringWithQualityHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of 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 + /// for a comparer for media type + /// q-values. + /// + internal class StringWithQualityHeaderValueComparer : IComparer + { + private static readonly StringWithQualityHeaderValueComparer _qualityComparer = + new StringWithQualityHeaderValueComparer(); + + private StringWithQualityHeaderValueComparer() + { + } + + public static StringWithQualityHeaderValueComparer QualityComparer + { + get { return _qualityComparer; } + } + + /// + /// Compares two 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 following their q-values ending up with any + /// wild-cards at the end. + /// + /// The first value to compare. + /// The second value to compare + /// The result of the comparison. + 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; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 4340aa8f21..209cd370da 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -44,9 +44,15 @@ + + + + + + diff --git a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/OutputFormatterDescriptorExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/OutputFormatterDescriptorExtensions.cs index f8d0b6edfd..1edb0d2b8a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/OutputFormatterDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/OutputFormatterDescriptorExtensions.cs @@ -13,10 +13,10 @@ namespace Microsoft.AspNet.Mvc public static class OutputFormatterDescriptorExtensions { /// - /// Adds a type representing a to a descriptor collection. + /// Adds a type representing a to a descriptor collection. /// /// A list of OutputFormatterDescriptors - /// Type representing an . + /// Type representing an . /// OutputFormatterDescriptor representing the added instance. public static OutputFormatterDescriptor Add([NotNull] this IList descriptors, [NotNull] Type outputFormatterType) @@ -27,10 +27,10 @@ namespace Microsoft.AspNet.Mvc } /// - /// Inserts a type representing a to a descriptor collection. + /// Inserts a type representing a to a descriptor collection. /// /// A list of OutputFormatterDescriptors - /// Type representing an . + /// Type representing an . /// OutputFormatterDescriptor representing the inserted instance. public static OutputFormatterDescriptor Insert([NotNull] this IList descriptors, int index, @@ -47,13 +47,13 @@ namespace Microsoft.AspNet.Mvc } /// - /// Adds an to a descriptor collection. + /// Adds an to a descriptor collection. /// /// A list of OutputFormatterDescriptors - /// An instance. + /// An instance. /// OutputFormatterDescriptor representing the added instance. public static OutputFormatterDescriptor Add([NotNull] this IList descriptors, - [NotNull] OutputFormatter outputFormatter) + [NotNull] IOutputFormatter outputFormatter) { var descriptor = new OutputFormatterDescriptor(outputFormatter); descriptors.Add(descriptor); @@ -61,14 +61,14 @@ namespace Microsoft.AspNet.Mvc } /// - /// Insert an to a descriptor collection. + /// Insert an to a descriptor collection. /// /// A list of OutputFormatterDescriptors - /// An instance. + /// An instance. /// OutputFormatterDescriptor representing the added instance. public static OutputFormatterDescriptor Insert([NotNull] this IList descriptors, int index, - [NotNull] OutputFormatter outputFormatter) + [NotNull] IOutputFormatter outputFormatter) { if (index < 0 || index > descriptors.Count) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index bd5145d266..0bc71cefec 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -315,11 +315,15 @@ 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. + + 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. + The following errors occurred with attribute routing information:{0}{0}{1} {0} is the newline. {1} is the formatted list of errors using AttributeRoute_IndividualErrorMessage - + + The attribute route '{0}' cannot contain a parameter named '{{{1}}}'. Use '[{1}]' in the route template to insert the value '{2}'. diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index cded969302..113a5f0933 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -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)); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 6e9a6ef834..fed6fc241b 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -66,10 +66,9 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); - yield return describe.Transient(); - yield return describe.Scoped(); - yield return describe.Transient(); - yield return describe.Scoped(); + yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Transient(); yield return describe.Transient, DefaultFilterProvider>(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs index 30bd79d967..15f57578c5 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectContentResultTests.cs @@ -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 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 contentTypes, string acceptHeader, string expectedHeader) + { + // Arrange + var expectedContentType = expectedHeader; + var input = "testInput"; + var stream = new MemoryStream(); + + var httpResponse = new Mock(); + var tempContentType = string.Empty; + httpResponse.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + + var actionContext = CreateMockActionContext(httpResponse.Object, acceptHeader); + var result = new ObjectResult(input); + + // Set the content type property explicitly. + result.ContentTypes = contentTypes.Select(contentType => MediaTypeHeaderValue.Parse(contentType)).ToList(); + result.Formatters = new List + { + new CannotWriteFormatter(), + new JsonOutputFormatter(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(); + 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 + { + 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 + { + 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())); + } + + [Fact] + public async Task ObjectResult_NoContentTypeSetWithAcceptHeaders_PicksFormatterOnAcceptHeaders() + { + // Arrange + var expectedContentType = "application/json;charset=utf-8"; + var input = "testInput"; + var stream = new MemoryStream(); + + var httpResponse = GetMockHttpResponse(); + var actionContext = + CreateMockActionContext(httpResponse.Object, + requestAcceptHeader: "text/custom;q=0.1,application/json;q=0.9", + requestContentType: "application/custom"); + var result = new ObjectResult(input); + + // Set more than one formatters. The test output formatter throws on write. + result.Formatters = new List + { + new CannotWriteFormatter(), + new JsonOutputFormatter(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.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + + var actionContext = CreateMockActionContext(httpResponse.Object, + requestAcceptHeader: null, + requestContentType: "application/json"); + var input = "testInput"; + var result = new ObjectResult(input); + + // Set more than one formatters. The test output formatter throws on write. + result.Formatters = new List + { + new CannotWriteFormatter(), + new JsonOutputFormatter(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.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + + var actionContext = CreateMockActionContext(httpResponse.Object, + requestAcceptHeader: null, + requestContentType: null); + var input = "testInput"; + var result = new ObjectResult(input); + + // Set more than one formatters. The test output formatter throws on write. + result.Formatters = new List + { + new CannotWriteFormatter(), + new JsonOutputFormatter(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.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + + var actionContext = CreateMockActionContext(httpResponse.Object, + requestAcceptHeader: null, + requestContentType: null); + var input = "testInput"; + var result = new ObjectResult(input); + + // Set more than one formatters. The test output formatter throws on write. + result.Formatters = new List + { + new CannotWriteFormatter(), + }; + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + // Asserts that content type is not text/custom. + httpResponse.VerifySet(r => r.StatusCode = 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.SetupSet(r => r.ContentType = expectedContentType).Verifiable(); + var tempContentType = string.Empty; + httpResponse.SetupProperty(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.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(); + var tempHttpResponse = new Mock(); + + tempHttpResponse.SetupGet(o => o.Body).Returns(tempStream); + tempHttpResponse.SetupProperty(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(); 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(); + 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 GetMockHttpResponse() + { + var stream = new MemoryStream(); + var httpResponse = new Mock(); + httpResponse.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + return httpResponse; + } + + private static Mock GetMockFormatter() + { + var mockFormatter = new Mock(); + mockFormatter.Setup(o => o.CanWriteResult(It.IsAny(), + It.IsAny())) + .Returns(true); + + mockFormatter.Setup(o => o.WriteAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + return mockFormatter; + } + + private static IServiceProvider GetServiceProvider() + { + var optionsSetup = new MvcOptionsSetup(); + var options = new MvcOptions(); + optionsSetup.Setup(options); + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance>(optionsAccessor.Object); + return serviceCollection.BuildServiceProvider(); + } + + public class CannotWriteFormatter : IOutputFormatter + { + public List SupportedEncodings + { + get + { + throw new NotImplementedException(); + } + } + + public List 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 OutputFormatters + { + get + { + return new List() + { new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: true) }; + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs index a8e85086b3..6e46e62ae6 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/OutputFormatterTests.cs @@ -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 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(); + 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(); + 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(() => testFormatter.SelectCharacterEncoding(testContentType)); - Assert.Equal("No encoding found for output formatter "+ - "'Microsoft.AspNet.Mvc.Test.OutputFormatterTests+TestFormatter'." + + var ex = Assert.Throws( + () => 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); } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/MediaTypeWithQualityHeaderValueTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/MediaTypeWithQualityHeaderValueTests.cs new file mode 100644 index 0000000000..9a952f12e5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/MediaTypeWithQualityHeaderValueTests.cs @@ -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 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 unsorted, IEnumerable expectedSorted) + { + // Arrange + var unsortedValues = + new List(unsorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u))); + + var expectedSortedValues = + new List(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); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 84c210b9d8..c3c2be983e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -46,6 +46,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorExtensionTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorExtensionTest.cs index a157638efe..18d7442130 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorExtensionTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorExtensionTest.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 { - new OutputFormatterDescriptor(Mock.Of()), - new OutputFormatterDescriptor(Mock.Of()) + new OutputFormatterDescriptor(Mock.Of()), + new OutputFormatterDescriptor(Mock.Of()) }; // Act & Assert - Assert.Throws("index", - () => collection.Insert(index, typeof(OutputFormatter))); + Assert.Throws("index", + () => collection.Insert(index, typeof(IOutputFormatter))); } [Theory] @@ -36,10 +36,10 @@ namespace Microsoft.AspNet.Mvc.Core.Test // Arrange var collection = new List { - new OutputFormatterDescriptor(Mock.Of()), - new OutputFormatterDescriptor(Mock.Of()) + new OutputFormatterDescriptor(Mock.Of()), + new OutputFormatterDescriptor(Mock.Of()) }; - var formatter = Mock.Of(); + var formatter = Mock.Of(); // Act & Assert Assert.Throws("index", () => collection.Insert(index, formatter)); @@ -49,8 +49,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test public void OutputFormatterDescriptors_AddsTypesAndInstances() { // Arrange - var formatter1 = Mock.Of(); - var formatter2 = Mock.Of(); + var formatter1 = Mock.Of(); + var formatter2 = Mock.Of(); var type1 = typeof(JsonOutputFormatter); var type2 = typeof(OutputFormatter); var collection = new List(); @@ -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 \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs index 3bf86d7a81..5b395b41cc 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs @@ -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); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 1a927656d0..0a4fcdc129 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -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-*", diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj index f2dc6d012b..2ddb6fbee8 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj @@ -24,6 +24,7 @@ + @@ -37,7 +38,6 @@ -