From 8966680075359c38abfc24ace30eb21953624543 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 15 Oct 2014 14:23:13 -0700 Subject: [PATCH] [Fixes #1332] Port IContentNegotiator to WebAPI shim for CoreCLR support 1. Ported IContentNegotiator. 2. Enabled Core CLR configuration --- .../ContentNegotiator/CollectionExtensions.cs | 275 ++++++ .../ContentNegotiationResult.cs | 50 ++ .../DefaultContentNegotiator.cs | 460 ++++++++++ .../ContentNegotiator/FormDataCollection.cs | 46 + .../ContentNegotiator/FormattingUtilities.cs | 241 +++++ .../ContentNegotiator/IContentNegotiator.cs | 34 + .../ListWrapperCollection.cs | 34 + .../ContentNegotiator/MediaTypeConstants.cs | 105 +++ .../MediaTypeFormatterMatch.cs | 52 ++ .../MediaTypeFormatterMatchRanking.cs | 48 + .../MediaTypeHeaderValueExtensions.cs | 110 +++ .../MediaTypeWithQualityHeaderComparer.cs | 112 +++ .../ParsedMediaTypeHeaderValue.cs | 75 ++ .../StringWithQualityHeaderValueComparer.cs | 77 ++ .../HttpRequestMessageExtensions.cs | 5 + .../project.json | 7 +- .../WebApiCompatShimActionSelectionTest.cs | 4 +- .../WebApiCompatShimBasicTest.cs | 6 +- .../project.json | 7 +- .../ApiControllerActionDiscoveryTest.cs | 5 +- .../DefaultContentNegotiatorTest.cs | 835 ++++++++++++++++++ .../HttpErrorTest.cs | 4 + .../HttpRequestMessageExtensionsTest.cs | 89 +- .../HttpResponseExceptionActionFilterTest.cs | 29 +- ...HttpResponseMessageOutputFormatterTests.cs | 7 +- .../Mocks/MockContentNegotiator.cs | 81 ++ .../Mocks/MockMediaTypeFormatter.cs | 47 + .../Mocks/MockMediaTypeMapping.cs | 37 + .../TestUtils/FlagsEnum.cs | 15 + .../TestUtils/LongEnum.cs | 13 + .../TestUtils/MediaTypeAssert.cs | 57 ++ .../TestUtils/MediaTypeConstants.cs | 102 +++ .../TestUtils/MediaTypeHeaderValueComparer.cs | 195 ++++ .../TestUtils/RefTypeTestData.cs | 76 ++ .../TestUtils/SimpleEnum.cs | 13 + .../TestUtils/TestData.cs | 441 +++++++++ .../TestUtils/TestDataHolder.cs | 51 ++ .../TestUtils/TestDataSetAttribute.cs | 205 +++++ .../TestUtils/TestDataVariations.cs | 116 +++ .../TestUtils/TypeAssert.cs | 165 ++++ .../TestUtils/ValueTypeTestData.cs | 40 + .../project.json | 13 +- .../WebApiCompatShimWebSite/project.json | 3 +- 43 files changed, 4317 insertions(+), 70 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/CollectionExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ContentNegotiationResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/DefaultContentNegotiator.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormDataCollection.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormattingUtilities.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/IContentNegotiator.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ListWrapperCollection.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeConstants.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatch.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatchRanking.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeHeaderValueExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeWithQualityHeaderComparer.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ParsedMediaTypeHeaderValue.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/StringWithQualityHeaderValueComparer.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/DefaultContentNegotiatorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockContentNegotiator.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeFormatter.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeMapping.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/FlagsEnum.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/LongEnum.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeAssert.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeConstants.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeHeaderValueComparer.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/RefTypeTestData.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/SimpleEnum.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestData.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataHolder.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataSetAttribute.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataVariations.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TypeAssert.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/ValueTypeTestData.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/CollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/CollectionExtensions.cs new file mode 100644 index 0000000000..c8d80c0048 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/CollectionExtensions.cs @@ -0,0 +1,275 @@ +// 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 ASPNETCORE50 + +using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace System.Collections.Generic +{ + /// + /// Helper extension methods for fast use of collections. + /// + internal static class CollectionExtensions + { + /// + /// Return a new array with the value added to the end. Slow and best suited to long lived arrays with few writes relative to reads. + /// + public static T[] AppendAndReallocate(this T[] array, T value) + { + Contract.Assert(array != null); + + int originalLength = array.Length; + T[] newArray = new T[originalLength + 1]; + array.CopyTo(newArray, 0); + newArray[originalLength] = value; + return newArray; + } + + /// + /// Return the enumerable as an Array, copying if required. Optimized for common case where it is an Array. + /// Avoid mutating the return value. + /// + public static T[] AsArray(this IEnumerable values) + { + Contract.Assert(values != null); + + T[] array = values as T[]; + if (array == null) + { + array = values.ToArray(); + } + return array; + } + + /// + /// Return the enumerable as a Collection of T, copying if required. Optimized for the common case where it is + /// a Collection of T and avoiding a copy if it implements IList of T. Avoid mutating the return value. + /// + public static Collection AsCollection(this IEnumerable enumerable) + { + Contract.Assert(enumerable != null); + + Collection collection = enumerable as Collection; + if (collection != null) + { + return collection; + } + // Check for IList so that collection can wrap it instead of copying + IList list = enumerable as IList; + if (list == null) + { + list = new List(enumerable); + } + return new Collection(list); + } + + /// + /// Return the enumerable as a IList of T, copying if required. Avoid mutating the return value. + /// + public static IList AsIList(this IEnumerable enumerable) + { + Contract.Assert(enumerable != null); + + IList list = enumerable as IList; + if (list != null) + { + return list; + } + return new List(enumerable); + } + + /// + /// Return the enumerable as a List of T, copying if required. Optimized for common case where it is an List of T + /// or a ListWrapperCollection of T. Avoid mutating the return value. + /// + public static List AsList(this IEnumerable enumerable) + { + Contract.Assert(enumerable != null); + + List list = enumerable as List; + if (list != null) + { + return list; + } + ListWrapperCollection listWrapper = enumerable as ListWrapperCollection; + if (listWrapper != null) + { + return listWrapper.ItemsList; + } + return new List(enumerable); + } + + /// + /// Remove values from the list starting at the index start. + /// + public static void RemoveFrom(this List list, int start) + { + Contract.Assert(list != null); + Contract.Assert(start >= 0 && start <= list.Count); + + list.RemoveRange(start, list.Count - start); + } + + /// + /// Return the only value from list, the type's default value if empty, or call the errorAction for 2 or more. + /// + public static T SingleDefaultOrError(this IList list, Action errorAction, TArg1 errorArg1) + { + Contract.Assert(list != null); + Contract.Assert(errorAction != null); + + switch (list.Count) + { + case 0: + return default(T); + + case 1: + T value = list[0]; + return value; + + default: + errorAction(errorArg1); + return default(T); + } + } + + /// + /// Returns a single value in list matching type TMatch if there is only one, null if there are none of type TMatch or calls the + /// errorAction with errorArg1 if there is more than one. + /// + public static TMatch SingleOfTypeDefaultOrError(this IList list, Action errorAction, TArg1 errorArg1) where TMatch : class + { + Contract.Assert(list != null); + Contract.Assert(errorAction != null); + + TMatch result = null; + for (int i = 0; i < list.Count; i++) + { + TMatch typedValue = list[i] as TMatch; + if (typedValue != null) + { + if (result == null) + { + result = typedValue; + } + else + { + errorAction(errorArg1); + return null; + } + } + } + return result; + } + + /// + /// Convert an ICollection to an array, removing null values. Fast path for case where there are no null values. + /// + public static T[] ToArrayWithoutNulls(this ICollection collection) where T : class + { + Contract.Assert(collection != null); + + T[] result = new T[collection.Count]; + int count = 0; + foreach (T value in collection) + { + if (value != null) + { + result[count] = value; + count++; + } + } + if (count == collection.Count) + { + return result; + } + else + { + T[] trimmedResult = new T[count]; + Array.Copy(result, trimmedResult, count); + return trimmedResult; + } + } + + /// + /// Convert the array to a Dictionary using the keySelector to extract keys from values and the specified comparer. Optimized for array input. + /// + public static Dictionary ToDictionaryFast(this TValue[] array, Func keySelector, IEqualityComparer comparer) + { + Contract.Assert(array != null); + Contract.Assert(keySelector != null); + + Dictionary dictionary = new Dictionary(array.Length, comparer); + for (int i = 0; i < array.Length; i++) + { + TValue value = array[i]; + dictionary.Add(keySelector(value), value); + } + return dictionary; + } + + /// + /// Convert the list to a Dictionary using the keySelector to extract keys from values and the specified comparer. Optimized for IList of T input with fast path for array. + /// + public static Dictionary ToDictionaryFast(this IList list, Func keySelector, IEqualityComparer comparer) + { + Contract.Assert(list != null); + Contract.Assert(keySelector != null); + + TValue[] array = list as TValue[]; + if (array != null) + { + return ToDictionaryFast(array, keySelector, comparer); + } + return ToDictionaryFastNoCheck(list, keySelector, comparer); + } + + /// + /// Convert the enumerable to a Dictionary using the keySelector to extract keys from values and the specified comparer. Fast paths for array and IList of T. + /// + public static Dictionary ToDictionaryFast(this IEnumerable enumerable, Func keySelector, IEqualityComparer comparer) + { + Contract.Assert(enumerable != null); + Contract.Assert(keySelector != null); + + TValue[] array = enumerable as TValue[]; + if (array != null) + { + return ToDictionaryFast(array, keySelector, comparer); + } + IList list = enumerable as IList; + if (list != null) + { + return ToDictionaryFastNoCheck(list, keySelector, comparer); + } + Dictionary dictionary = new Dictionary(comparer); + foreach (TValue value in enumerable) + { + dictionary.Add(keySelector(value), value); + } + return dictionary; + } + + /// + /// Convert the list to a Dictionary using the keySelector to extract keys from values and the specified comparer. Optimized for IList of T input. No checking for other types. + /// + private static Dictionary ToDictionaryFastNoCheck(IList list, Func keySelector, IEqualityComparer comparer) + { + Contract.Assert(list != null); + Contract.Assert(keySelector != null); + + int listCount = list.Count; + Dictionary dictionary = new Dictionary(listCount, comparer); + for (int i = 0; i < listCount; i++) + { + TValue value = list[i]; + dictionary.Add(keySelector(value), value); + } + return dictionary; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ContentNegotiationResult.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ContentNegotiationResult.cs new file mode 100644 index 0000000000..1db747bba8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ContentNegotiationResult.cs @@ -0,0 +1,50 @@ +// 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 ASPNETCORE50 + +using Microsoft.AspNet.Mvc; +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Web.Http; + +namespace System.Net.Http.Formatting +{ + /// + /// Represents the result of content negotiation performed using + /// + /// + public class ContentNegotiationResult + { + private MediaTypeFormatter _formatter; + + /// + /// Create the content negotiation result object. + /// + /// The formatter. + /// The preferred media type. Can be null. + public ContentNegotiationResult([NotNull] MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType) + { + _formatter = formatter; + MediaType = mediaType; + } + + /// + /// The formatter chosen for serialization. + /// + public MediaTypeFormatter Formatter + { + get { return _formatter; } + set + { + _formatter = value; + } + } + + /// + /// The media type that is associated with the formatter chosen for serialization. Can be null. + /// + public MediaTypeHeaderValue MediaType { get; set; } + } +} +#endif diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/DefaultContentNegotiator.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/DefaultContentNegotiator.cs new file mode 100644 index 0000000000..d8d8c5e1cd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/DefaultContentNegotiator.cs @@ -0,0 +1,460 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Web.Http; +using Microsoft.AspNet.Mvc; +using System.Net.Http.Formatting; + +namespace System.Net.Http.Formatting +{ + /// + /// Class that selects a for an + /// or . + /// + public class DefaultContentNegotiator : IContentNegotiator + { + public DefaultContentNegotiator() + : this(false) + { + } + + /// + /// Initializes a new instance of the with + /// the given setting for . + /// + /// + /// If ExcludeMatchOnTypeOnly is true then we don't match on type only which means + /// that we return null if we can't match on anything in the request. This is useful + /// for generating 406 (Not Acceptable) status codes. + /// + public DefaultContentNegotiator(bool excludeMatchOnTypeOnly) + { + ExcludeMatchOnTypeOnly = excludeMatchOnTypeOnly; + } + + /// + /// If ExcludeMatchOnTypeOnly is true then we don't match on type only which means + /// that we return null if we can't match on anything in the request. This is useful + /// for generating 406 (Not Acceptable) status codes. + /// + public bool ExcludeMatchOnTypeOnly { get; private set; } + + /// + /// Performs content negotiating by selecting the most appropriate out of the passed in + /// for the given that can serialize an object of the given + /// . + /// + /// The type to be serialized. + /// The request. + /// The set of objects from which to choose. + /// The result of the negotiation containing the most appropriate instance, + /// or null if there is no appropriate formatter. + public virtual ContentNegotiationResult Negotiate([NotNull] Type type, [NotNull] HttpRequestMessage request, [NotNull] IEnumerable formatters) + { + // Go through each formatter to compute how well it matches. + Collection matches = ComputeFormatterMatches(type, request, formatters); + + // Select best formatter match among the matches + MediaTypeFormatterMatch bestFormatterMatch = SelectResponseMediaTypeFormatter(matches); + + // We found a best formatter + if (bestFormatterMatch != null) + { + // Find the best character encoding for the selected formatter + Encoding bestEncodingMatch = SelectResponseCharacterEncoding(request, bestFormatterMatch.Formatter); + if (bestEncodingMatch != null) + { + bestFormatterMatch.MediaType.CharSet = bestEncodingMatch.WebName; + } + + MediaTypeHeaderValue bestMediaType = bestFormatterMatch.MediaType; + MediaTypeFormatter bestFormatter = bestFormatterMatch.Formatter.GetPerRequestFormatterInstance(type, request, bestMediaType); + return new ContentNegotiationResult(bestFormatter, bestMediaType); + } + + return null; + } + + /// + /// Determine how well each formatter matches by associating a value + /// with the formatter. Then associate the quality of the match based on q-factors and other parameters. The result of this + /// method is a collection of the matches found categorized and assigned a quality value. + /// + /// The type to be serialized. + /// The request. + /// The set of objects from which to choose. + /// A collection containing all the matches. + protected virtual Collection ComputeFormatterMatches([NotNull] Type type, [NotNull] HttpRequestMessage request, [NotNull] IEnumerable formatters) + { + IEnumerable sortedAcceptValues = null; + + // Go through each formatter to find how well it matches. + ListWrapperCollection matches = new ListWrapperCollection(); + MediaTypeFormatter[] writingFormatters = GetWritingFormatters(formatters); + for (int i = 0; i < writingFormatters.Length; i++) + { + MediaTypeFormatter formatter = writingFormatters[i]; + MediaTypeFormatterMatch match = null; + + // Check first that formatter can write the actual type + if (!formatter.CanWriteType(type)) + { + // Formatter can't even write the type so no match at all + continue; + } + + // Match against the accept header values. + if (sortedAcceptValues == null) + { + // Sort the Accept header values in descending order based on q-factor + sortedAcceptValues = SortMediaTypeWithQualityHeaderValuesByQFactor(request.Headers.Accept); + } + if ((match = MatchAcceptHeader(sortedAcceptValues, formatter)) != null) + { + matches.Add(match); + continue; + } + + // Match against request's media type if any + if ((match = MatchRequestMediaType(request, formatter)) != null) + { + matches.Add(match); + continue; + } + + // Check whether we should match on type or stop the matching process. + // The latter is used to generate 406 (Not Acceptable) status codes. + bool shouldMatchOnType = ShouldMatchOnType(sortedAcceptValues); + + // Match against the type of object we are writing out + if (shouldMatchOnType && (match = MatchType(type, formatter)) != null) + { + matches.Add(match); + continue; + } + } + + return matches; + } + + /// + /// Select the best match among the candidate matches found. + /// + /// The collection of matches. + /// The determined to be the best match. + protected virtual MediaTypeFormatterMatch SelectResponseMediaTypeFormatter([NotNull] ICollection matches) + { + // Performance-sensitive + + List matchList = matches.AsList(); + + MediaTypeFormatterMatch bestMatchOnType = null; + MediaTypeFormatterMatch bestMatchOnAcceptHeaderLiteral = null; + MediaTypeFormatterMatch bestMatchOnAcceptHeaderSubtypeMediaRange = null; + MediaTypeFormatterMatch bestMatchOnAcceptHeaderAllMediaRange = null; + MediaTypeFormatterMatch bestMatchOnMediaTypeMapping = null; + MediaTypeFormatterMatch bestMatchOnRequestMediaType = null; + + // Go through each formatter to find the best match in each category. + for (int i = 0; i < matchList.Count; i++) + { + MediaTypeFormatterMatch match = matchList[i]; + switch (match.Ranking) + { + case MediaTypeFormatterMatchRanking.MatchOnCanWriteType: + // First match by type trumps all other type matches + if (bestMatchOnType == null) + { + bestMatchOnType = match; + } + break; + + case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral: + // Matches on accept headers must choose the highest quality match. + // A match of 0.0 means we won't use it at all. + bestMatchOnAcceptHeaderLiteral = UpdateBestMatch(bestMatchOnAcceptHeaderLiteral, match); + break; + + case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange: + // Matches on accept headers must choose the highest quality match. + // A match of 0.0 means we won't use it at all. + bestMatchOnAcceptHeaderSubtypeMediaRange = UpdateBestMatch(bestMatchOnAcceptHeaderSubtypeMediaRange, match); + break; + + case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange: + // Matches on accept headers must choose the highest quality match. + // A match of 0.0 means we won't use it at all. + bestMatchOnAcceptHeaderAllMediaRange = UpdateBestMatch(bestMatchOnAcceptHeaderAllMediaRange, match); + break; + + case MediaTypeFormatterMatchRanking.MatchOnRequestMediaType: + // First match on request content type trumps other request content matches + if (bestMatchOnRequestMediaType == null) + { + bestMatchOnRequestMediaType = match; + } + break; + } + } + + // If we received matches based on both supported media types and from media type mappings, + // we want to give precedence to the media type mappings, but only if their quality is >= that of the supported media type. + // We do this because media type mappings are the user's extensibility point and must take precedence over normal + // supported media types in the case of a tie. The 99% case is where both have quality 1.0. + if (bestMatchOnMediaTypeMapping != null) + { + MediaTypeFormatterMatch mappingOverride = bestMatchOnMediaTypeMapping; + mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderLiteral); + mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderSubtypeMediaRange); + mappingOverride = UpdateBestMatch(mappingOverride, bestMatchOnAcceptHeaderAllMediaRange); + if (mappingOverride != bestMatchOnMediaTypeMapping) + { + bestMatchOnMediaTypeMapping = null; + } + } + + // now select the formatter and media type + // A MediaTypeMapping is highest precedence -- it is an extensibility point + // allowing the user to override normal accept header matching + MediaTypeFormatterMatch bestMatch = null; + if (bestMatchOnMediaTypeMapping != null) + { + bestMatch = bestMatchOnMediaTypeMapping; + } + else if (bestMatchOnAcceptHeaderLiteral != null || + bestMatchOnAcceptHeaderSubtypeMediaRange != null || + bestMatchOnAcceptHeaderAllMediaRange != null) + { + bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderLiteral); + bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderSubtypeMediaRange); + bestMatch = UpdateBestMatch(bestMatch, bestMatchOnAcceptHeaderAllMediaRange); + } + else if (bestMatchOnRequestMediaType != null) + { + bestMatch = bestMatchOnRequestMediaType; + } + else if (bestMatchOnType != null) + { + bestMatch = bestMatchOnType; + } + + return bestMatch; + } + + /// + /// Determine the best character encoding for writing the response. First we look + /// for accept-charset headers and if not found then we try to match + /// any charset encoding in the request (in case of PUT, POST, etc.) + /// If no encoding is found then we use the default for the formatter. + /// + /// The determined to be the best match. + protected virtual Encoding SelectResponseCharacterEncoding([NotNull] HttpRequestMessage request, [NotNull] MediaTypeFormatter formatter) + { + // If there are any SupportedEncodings then we pick an encoding + List supportedEncodings = formatter.SupportedEncodings.ToList(); + if (supportedEncodings.Count > 0) + { + // Sort Accept-Charset header values + IEnumerable sortedAcceptCharsetValues = SortStringWithQualityHeaderValuesByQFactor(request.Headers.AcceptCharset); + + // Check for match based on accept-charset headers + foreach (StringWithQualityHeaderValue acceptCharset in sortedAcceptCharsetValues) + { + for (int i = 0; i < supportedEncodings.Count; i++) + { + Encoding encoding = supportedEncodings[i]; + if (encoding != null && acceptCharset.Quality != FormattingUtilities.NoMatch && + (acceptCharset.Value.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) || + acceptCharset.Value.Equals("*", StringComparison.OrdinalIgnoreCase))) + { + return encoding; + } + } + } + + // Check for match based on any request entity body + return formatter.SelectCharacterEncoding(request.Content != null ? request.Content.Headers : null); + } + + return null; + } + + /// + /// Match the request accept header field values against the formatter's registered supported media types. + /// + /// The sorted accept header values to match. + /// The formatter to match against. + /// A indicating the quality of the match or null is no match. + protected virtual MediaTypeFormatterMatch MatchAcceptHeader([NotNull] IEnumerable sortedAcceptValues, [NotNull] MediaTypeFormatter formatter) + { + foreach (MediaTypeWithQualityHeaderValue acceptMediaTypeValue in sortedAcceptValues) + { + List supportedMediaTypes = formatter.SupportedMediaTypes.ToList(); + for (int i = 0; i < supportedMediaTypes.Count; i++) + { + MediaTypeHeaderValue supportedMediaType = supportedMediaTypes[i]; + MediaTypeHeaderValueRange range; + if (supportedMediaType != null && acceptMediaTypeValue.Quality != FormattingUtilities.NoMatch && + supportedMediaType.IsSubsetOf(acceptMediaTypeValue, out range)) + { + MediaTypeFormatterMatchRanking ranking; + switch (range) + { + case MediaTypeHeaderValueRange.AllMediaRange: + ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange; + break; + + case MediaTypeHeaderValueRange.SubtypeMediaRange: + ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange; + break; + + default: + ranking = MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral; + break; + } + + return new MediaTypeFormatterMatch(formatter, supportedMediaType, acceptMediaTypeValue.Quality, ranking); + } + } + } + + return null; + } + + /// + /// Match any request media type (in case there is a request entity body) against the formatter's registered + /// media types. + /// + /// The request to match. + /// The formatter to match against. + /// A indicating the quality of the match or null is no match. + protected virtual MediaTypeFormatterMatch MatchRequestMediaType([NotNull] HttpRequestMessage request, [NotNull] MediaTypeFormatter formatter) + { + if (request.Content != null) + { + MediaTypeHeaderValue requestMediaType = request.Content.Headers.ContentType; + if (requestMediaType != null) + { + List supportedMediaTypes = formatter.SupportedMediaTypes.ToList(); + for (int i = 0; i < supportedMediaTypes.Count; i++) + { + MediaTypeHeaderValue supportedMediaType = supportedMediaTypes[i]; + if (supportedMediaType != null && supportedMediaType.IsSubsetOf(requestMediaType)) + { + return new MediaTypeFormatterMatch(formatter, supportedMediaType, FormattingUtilities.Match, MediaTypeFormatterMatchRanking.MatchOnRequestMediaType); + } + } + } + } + + return null; + } + + /// + /// Determine whether to match on type or not. This is used to determine whether to + /// generate a 406 response or use the default media type formatter in case there + /// is no match against anything in the request. If ExcludeMatchOnTypeOnly is true + /// then we don't match on type unless there are no accept headers. + /// + /// The sorted accept header values to match. + /// True if not ExcludeMatchOnTypeOnly and accept headers with a q-factor bigger than 0.0 are present. + protected virtual bool ShouldMatchOnType([NotNull] IEnumerable sortedAcceptValues) + { + return !(ExcludeMatchOnTypeOnly && sortedAcceptValues.Any()); + } + + /// + /// Pick the first supported media type and indicate we've matched only on type + /// + /// The type to be serialized. + /// The formatter we are matching against. + /// A indicating the quality of the match or null is no match. + protected virtual MediaTypeFormatterMatch MatchType([NotNull] Type type, [NotNull] MediaTypeFormatter formatter) + { + // We already know that we do match on type -- otherwise we wouldn't even be called -- + // so this is just a matter of determining how we match. + MediaTypeHeaderValue mediaType = null; + List supportedMediaTypes = formatter.SupportedMediaTypes.ToList(); + if (supportedMediaTypes.Count > 0) + { + mediaType = supportedMediaTypes[0]; + } + return new MediaTypeFormatterMatch(formatter, mediaType, FormattingUtilities.Match, MediaTypeFormatterMatchRanking.MatchOnCanWriteType); + } + + /// + /// Sort Accept header values and related header field values with similar syntax rules + /// (if more than 1) in descending order based on q-factor. + /// + /// The header values to sort. + /// The sorted header values. + protected virtual IEnumerable SortMediaTypeWithQualityHeaderValuesByQFactor(ICollection headerValues) + { + if (headerValues.Count > 1) + { + // 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(m => m, MediaTypeWithQualityHeaderValueComparer.QualityComparer).ToArray(); + } + else + { + return headerValues; + } + } + + /// + /// Sort Accept-Charset, Accept-Encoding, Accept-Language and related header field values with similar syntax rules + /// (if more than 1) in descending order based on q-factor. + /// + /// The header values to sort. + /// The sorted header values. + protected virtual IEnumerable SortStringWithQualityHeaderValuesByQFactor([NotNull] ICollection headerValues) + { + if (headerValues.Count > 1) + { + // 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(m => m, StringWithQualityHeaderValueComparer.QualityComparer).ToArray(); + } + else + { + return headerValues; + } + } + + /// + /// Evaluates whether a match is better than the current match and if so returns the replacement; otherwise returns the + /// current match. + /// + protected virtual MediaTypeFormatterMatch UpdateBestMatch(MediaTypeFormatterMatch current, MediaTypeFormatterMatch potentialReplacement) + { + if (potentialReplacement == null) + { + return current; + } + + if (current != null) + { + return (potentialReplacement.Quality > current.Quality) ? potentialReplacement : current; + } + + return potentialReplacement; + } + + private static MediaTypeFormatter[] GetWritingFormatters(IEnumerable formatters) + { + Contract.Assert(formatters != null); + MediaTypeFormatterCollection formatterCollection = formatters as MediaTypeFormatterCollection; + return formatters.AsArray(); + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormDataCollection.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormDataCollection.cs new file mode 100644 index 0000000000..e47e012654 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormDataCollection.cs @@ -0,0 +1,46 @@ +// 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 ASPNETCORE50 + +using Microsoft.AspNet.WebUtilities; +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; + +namespace System.Net.Http.Formatting +{ + public class FormDataCollection : IEnumerable> + { + private readonly IList> _values; + + public FormDataCollection(string query) + { + var parsedQuery = QueryHelpers.ParseQuery(query); + + var values = new List>(); + foreach (var kvp in parsedQuery) + { + foreach (var value in kvp.Value) + { + values.Add(new KeyValuePair(kvp.Key, value)); + } + } + + _values = values; + } + + public IEnumerator> GetEnumerator() + { + return _values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _values.GetEnumerator(); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormattingUtilities.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormattingUtilities.cs new file mode 100644 index 0000000000..82fec1ad8c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/FormattingUtilities.cs @@ -0,0 +1,241 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Xml; +using Newtonsoft.Json.Linq; +using System.Reflection; + +namespace System.Net.Http +{ + /// + /// Provides various internal utility functions + /// + internal static class FormattingUtilities + { + // Supported date formats for input. + private static readonly string[] dateFormats = new string[] + { + // "r", // RFC 1123, required output format but too strict for input + "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) + "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT + "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week + "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone + "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year + "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone + "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year + "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone + + "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 + "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone + "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format + + "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 + "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone + "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week + "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone + }; + + // Valid header token characters are within the range 0x20 < c < 0x7F excluding the following characters + private const string NonTokenChars = "()<>@,;:\\\"/[]?={}"; + + /// + /// Quality factor to indicate a perfect match. + /// + public const double Match = 1.0; + + /// + /// Quality factor to indicate no match. + /// + public const double NoMatch = 0.0; + + /// + /// The default max depth for our formatter is 256 + /// + public const int DefaultMaxDepth = 256; + + /// + /// The default min depth for our formatter is 1 + /// + public const int DefaultMinDepth = 1; + + /// + /// HTTP X-Requested-With header field name + /// + public const string HttpRequestedWithHeader = @"x-requested-with"; + + /// + /// HTTP X-Requested-With header field value + /// + public const string HttpRequestedWithHeaderValue = @"XMLHttpRequest"; + + /// + /// HTTP Host header field name + /// + public const string HttpHostHeader = "Host"; + + /// + /// HTTP Version token + /// + public const string HttpVersionToken = "HTTP"; + + /// + /// A representing . + /// + public static readonly Type HttpRequestMessageType = typeof(HttpRequestMessage); + + /// + /// A representing . + /// + public static readonly Type HttpResponseMessageType = typeof(HttpResponseMessage); + + /// + /// A representing . + /// + public static readonly Type HttpContentType = typeof(HttpContent); + + /// + /// A representing . + /// + public static readonly Type DelegatingEnumerableGenericType = typeof(DelegatingEnumerable<>); + + /// + /// A representing . + /// + public static readonly Type EnumerableInterfaceGenericType = typeof(IEnumerable<>); + + /// + /// A representing . + /// + public static readonly Type QueryableInterfaceGenericType = typeof(IQueryable<>); + + /// + /// Determines whether is a type. + /// + /// The type to test. + /// + /// true if is a type; otherwise, false. + /// + public static bool IsJTokenType(Type type) + { + return typeof(JToken).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()); + } + + /// + /// Creates an empty instance. The only way is to get it from a dummy + /// instance. + /// + /// The created instance. + public static HttpContentHeaders CreateEmptyContentHeaders() + { + HttpContent tempContent = null; + HttpContentHeaders contentHeaders = null; + try + { + tempContent = new StringContent(String.Empty); + contentHeaders = tempContent.Headers; + contentHeaders.Clear(); + } + finally + { + // We can dispose the content without touching the headers + if (tempContent != null) + { + tempContent.Dispose(); + } + } + + return contentHeaders; + } + + /// + /// Create a default reader quotas with a default depth quota of 1K + /// + /// + public static XmlDictionaryReaderQuotas CreateDefaultReaderQuotas() + { +#if NETFX_CORE // MaxDepth is a DOS mitigation. We don't support MaxDepth in portable libraries because it is strictly client side. + return XmlDictionaryReaderQuotas.Max; +#else + return new XmlDictionaryReaderQuotas() + { + MaxArrayLength = Int32.MaxValue, + MaxBytesPerRead = Int32.MaxValue, + MaxDepth = DefaultMaxDepth, + MaxNameTableCharCount = Int32.MaxValue, + MaxStringContentLength = Int32.MaxValue + }; +#endif + } + + /// + /// Remove bounding quotes on a token if present + /// + /// Token to unquote. + /// Unquoted token. + public static string UnquoteToken(string token) + { + if (String.IsNullOrWhiteSpace(token)) + { + return token; + } + + if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) + { + return token.Substring(1, token.Length - 2); + } + + return token; + } + + public static bool ValidateHeaderToken(string token) + { + if (token == null) + { + return false; + } + + foreach (char c in token) + { + if (c < 0x21 || c > 0x7E || NonTokenChars.IndexOf(c) != -1) + { + return false; + } + } + + return true; + } + + public static string DateToString(DateTimeOffset dateTime) + { + // Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo) + return dateTime.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture); + } + + public static bool TryParseDate(string input, out DateTimeOffset result) + { + return DateTimeOffset.TryParseExact(input, dateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, + out result); + } + + /// + /// Parses valid integer strings with no leading signs, whitespace or other + /// + /// The value to parse + /// The result + /// True if value was valid; false otherwise. + public static bool TryParseInt32(string value, out int result) + { + return Int32.TryParse(value, NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/IContentNegotiator.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/IContentNegotiator.cs new file mode 100644 index 0000000000..a51371cea0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/IContentNegotiator.cs @@ -0,0 +1,34 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// + /// Performs content negotiation. + /// This is the process of selecting a response writer (formatter) in compliance with header values in the request. + /// + public interface IContentNegotiator + { + /// + /// Performs content negotiating by selecting the most appropriate out of the passed in + /// for the given that can serialize an object of the given + /// . + /// + /// + /// Implementations of this method should call + /// on the selected formatter and return the result of that method. + /// + /// The type to be serialized. + /// Request message, which contains the header values used to perform negotiation. + /// The set of objects from which to choose. + /// The result of the negotiation containing the most appropriate instance, + /// or null if there is no appropriate formatter. + ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable formatters); + } +} +#endif diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ListWrapperCollection.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ListWrapperCollection.cs new file mode 100644 index 0000000000..aed400241c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ListWrapperCollection.cs @@ -0,0 +1,34 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; + +namespace System.Collections.ObjectModel +{ + /// + /// A class that inherits from Collection of T but also exposes its underlying data as List of T for performance. + /// + internal sealed class ListWrapperCollection : Collection + { + private readonly List _items; + + internal ListWrapperCollection() + : this(new List()) + { + } + + internal ListWrapperCollection(List list) + : base(list) + { + _items = list; + } + + internal List ItemsList + { + get { return _items; } + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeConstants.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeConstants.cs new file mode 100644 index 0000000000..d78f6f66aa --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeConstants.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. + +#if ASPNETCORE50 + +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// + /// Constants related to media types. + /// + internal static class MediaTypeConstants + { + private static readonly MediaTypeHeaderValue _defaultApplicationXmlMediaType = new MediaTypeHeaderValue("application/xml"); + private static readonly MediaTypeHeaderValue _defaultTextXmlMediaType = new MediaTypeHeaderValue("text/xml"); + private static readonly MediaTypeHeaderValue _defaultApplicationJsonMediaType = new MediaTypeHeaderValue("application/json"); + private static readonly MediaTypeHeaderValue _defaultTextJsonMediaType = new MediaTypeHeaderValue("text/json"); + private static readonly MediaTypeHeaderValue _defaultApplicationOctetStreamMediaType = new MediaTypeHeaderValue("application/octet-stream"); + private static readonly MediaTypeHeaderValue _defaultApplicationFormUrlEncodedMediaType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + private static readonly MediaTypeHeaderValue _defaultApplicationBsonMediaType = new MediaTypeHeaderValue("application/bson"); + + /// + /// Gets a instance representing application/octet-stream. + /// + /// + /// A new instance representing application/octet-stream. + /// + public static MediaTypeHeaderValue ApplicationOctetStreamMediaType + { + get { return _defaultApplicationOctetStreamMediaType; } + } + + /// + /// Gets a instance representing application/xml. + /// + /// + /// A new instance representing application/xml. + /// + public static MediaTypeHeaderValue ApplicationXmlMediaType + { + get { return _defaultApplicationXmlMediaType; } + } + + /// + /// Gets a instance representing application/json. + /// + /// + /// A new instance representing application/json. + /// + public static MediaTypeHeaderValue ApplicationJsonMediaType + { + get { return _defaultApplicationJsonMediaType; } + } + + /// + /// Gets a instance representing text/xml. + /// + /// + /// A new instance representing text/xml. + /// + public static MediaTypeHeaderValue TextXmlMediaType + { + get { return _defaultTextXmlMediaType; } + } + + /// + /// Gets a instance representing text/json. + /// + /// + /// A new instance representing text/json. + /// + public static MediaTypeHeaderValue TextJsonMediaType + { + get { return _defaultTextJsonMediaType; } + } + + /// + /// Gets a instance representing application/x-www-form-urlencoded. + /// + /// + /// A new instance representing application/x-www-form-urlencoded. + /// + public static MediaTypeHeaderValue ApplicationFormUrlEncodedMediaType + { + get { return _defaultApplicationFormUrlEncodedMediaType; } + } + + /// + /// Gets a instance representing application/bson. + /// + /// + /// A new instance representing application/bson. + /// + /// + /// Not yet a standard. In particular this media type is not currently listed at + /// http://www.iana.org/assignments/media-types/application. + /// + public static MediaTypeHeaderValue ApplicationBsonMediaType + { + get { return _defaultApplicationBsonMediaType; } + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatch.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatch.cs new file mode 100644 index 0000000000..12739383c1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatch.cs @@ -0,0 +1,52 @@ +// 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 ASPNETCORE50 + +using System.Net.Http.Headers; +using System.Web.Http; + +namespace System.Net.Http.Formatting +{ + /// + /// This class describes how well a particular matches a request. + /// + public class MediaTypeFormatterMatch + { + /// + /// Initializes a new instance of the class. + /// + /// The matching formatter. + /// The media type. Can be null in which case the media type application/octet-stream is used. + /// The quality of the match. Can be null in which case it is considered a full match with a value of 1.0 + /// The kind of match. + public MediaTypeFormatterMatch(MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType, double? quality, MediaTypeFormatterMatchRanking ranking) + { + Formatter = formatter; + MediaType = mediaType != null ? mediaType : MediaTypeConstants.ApplicationOctetStreamMediaType; + Quality = quality ?? FormattingUtilities.Match; + Ranking = ranking; + } + + /// + /// Gets the media type formatter. + /// + public MediaTypeFormatter Formatter { get; private set; } + + /// + /// Gets the matched media type. + /// + public MediaTypeHeaderValue MediaType { get; private set; } + + /// + /// Gets the quality of the match + /// + public double Quality { get; private set; } + + /// + /// Gets the kind of match that occurred. + /// + public MediaTypeFormatterMatchRanking Ranking { get; private set; } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatchRanking.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatchRanking.cs new file mode 100644 index 0000000000..11b23a1484 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeFormatterMatchRanking.cs @@ -0,0 +1,48 @@ +// 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 ASPNETCORE50 + +namespace System.Net.Http.Formatting +{ + /// + /// Contains information about the degree to which a matches the + /// explicit or implicit preferences found in an incoming request. + /// + public enum MediaTypeFormatterMatchRanking + { + /// + /// No match was found + /// + None = 0, + + /// + /// Matched on type meaning that the formatter is able to serialize the type + /// + MatchOnCanWriteType, + + /// + /// Matched on explicit literal accept header in , + /// e.g. "application/json". + /// + MatchOnRequestAcceptHeaderLiteral, + + /// + /// Matched on explicit subtype range accept header in , + /// e.g. "application/*". + /// + MatchOnRequestAcceptHeaderSubtypeMediaRange, + + /// + /// Matched on explicit all media type range accept header in , + /// e.g. "*/*" + /// + MatchOnRequestAcceptHeaderAllMediaRange, + + /// + /// Matched on the media type of the of the . + /// + MatchOnRequestMediaType, + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeHeaderValueExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeHeaderValueExtensions.cs new file mode 100644 index 0000000000..beb2b7a6cf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeHeaderValueExtensions.cs @@ -0,0 +1,110 @@ +// 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 ASPNETCORE50 + +using Microsoft.AspNet.Mvc; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// + /// Extension methods for . + /// + internal static class MediaTypeHeaderValueExtensions + { + /// + /// Determines whether two instances match. The instance + /// is said to match if and only if + /// is a strict subset of the values and parameters of . + /// That is, if the media type and media type parameters of are all present + /// and match those of then it is a match even though may have additional + /// parameters. + /// + /// The first media type. + /// The second media type. + /// true if this is a subset of ; false otherwise. + public static bool IsSubsetOf(this MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2) + { + MediaTypeHeaderValueRange mediaType2Range; + return IsSubsetOf(mediaType1, mediaType2, out mediaType2Range); + } + + /// + /// Determines whether two instances match. The instance + /// is said to match if and only if + /// is a strict subset of the values and parameters of . + /// That is, if the media type and media type parameters of are all present + /// and match those of then it is a match even though may have additional + /// parameters. + /// + /// The first media type. + /// The second media type. + /// Indicates whether is a regular media type, a subtype media range, or a full media range + /// true if this is a subset of ; false otherwise. + public static bool IsSubsetOf(this MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2, out MediaTypeHeaderValueRange mediaType2Range) + { + // Performance-sensitive + Contract.Assert(mediaType1 != null); + + if (mediaType2 == null) + { + mediaType2Range = MediaTypeHeaderValueRange.None; + return false; + } + + ParsedMediaTypeHeaderValue parsedMediaType1 = new ParsedMediaTypeHeaderValue(mediaType1); + ParsedMediaTypeHeaderValue parsedMediaType2 = new ParsedMediaTypeHeaderValue(mediaType2); + mediaType2Range = parsedMediaType2.IsAllMediaRange ? MediaTypeHeaderValueRange.AllMediaRange : + parsedMediaType2.IsSubtypeMediaRange ? MediaTypeHeaderValueRange.SubtypeMediaRange : + MediaTypeHeaderValueRange.None; + + if (!parsedMediaType1.TypesEqual(ref parsedMediaType2)) + { + if (mediaType2Range != MediaTypeHeaderValueRange.AllMediaRange) + { + return false; + } + } + else if (!parsedMediaType1.SubTypesEqual(ref parsedMediaType2)) + { + if (mediaType2Range != MediaTypeHeaderValueRange.SubtypeMediaRange) + { + return false; + } + } + + // So far we either have a full match or a subset match. Now check that all of + // mediaType1's parameters are present and equal in mediatype2 + // Optimize for the common case where the parameters inherit from Collection and cache the count which is faster for Collection. + Collection parameters1 = mediaType1.Parameters.AsCollection(); + int parameterCount1 = parameters1.Count; + Collection parameters2 = mediaType2.Parameters.AsCollection(); + int parameterCount2 = parameters2.Count; + for (int i = 0; i < parameterCount1; i++) + { + NameValueHeaderValue parameter1 = parameters1[i]; + bool found = false; + for (int j = 0; j < parameterCount2; j++) + { + NameValueHeaderValue parameter2 = parameters2[j]; + if (parameter1.Equals(parameter2)) + { + found = true; + break; + } + } + if (!found) + { + return false; + } + } + return true; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeWithQualityHeaderComparer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeWithQualityHeaderComparer.cs new file mode 100644 index 0000000000..8ac2c8d48b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/MediaTypeWithQualityHeaderComparer.cs @@ -0,0 +1,112 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// Implementation of that can compare accept media type header fields + /// based on their quality values (a.k.a q-values). See + /// for a comparer for other content negotiation + /// header field q-values. + internal 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) + { + Contract.Assert(mediaType1 != null, "The 'mediaType1' parameter should not be null."); + Contract.Assert(mediaType2 != null, "The 'mediaType2' parameter should not be null."); + + if (Object.ReferenceEquals(mediaType1, mediaType2)) + { + return 0; + } + + int returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2); + + if (returnValue == 0) + { + ParsedMediaTypeHeaderValue parsedMediaType1 = new ParsedMediaTypeHeaderValue(mediaType1); + ParsedMediaTypeHeaderValue parsedMediaType2 = new ParsedMediaTypeHeaderValue(mediaType2); + + if (!parsedMediaType1.TypesEqual(ref parsedMediaType2)) + { + if (parsedMediaType1.IsAllMediaRange) + { + return -1; + } + else if (parsedMediaType2.IsAllMediaRange) + { + return 1; + } + else if (parsedMediaType1.IsSubtypeMediaRange && !parsedMediaType2.IsSubtypeMediaRange) + { + return -1; + } + else if (!parsedMediaType1.IsSubtypeMediaRange && parsedMediaType2.IsSubtypeMediaRange) + { + return 1; + } + } + else if (!parsedMediaType1.SubTypesEqual(ref parsedMediaType2)) + { + if (parsedMediaType1.IsSubtypeMediaRange) + { + return -1; + } + else if (parsedMediaType2.IsSubtypeMediaRange) + { + return 1; + } + } + } + + return returnValue; + } + + private static int CompareBasedOnQualityFactor(MediaTypeWithQualityHeaderValue mediaType1, MediaTypeWithQualityHeaderValue mediaType2) + { + Contract.Assert(mediaType1 != null); + Contract.Assert(mediaType2 != null); + + double mediaType1Quality = mediaType1.Quality ?? FormattingUtilities.Match; + double mediaType2Quality = mediaType2.Quality ?? FormattingUtilities.Match; + double qualityDifference = mediaType1Quality - mediaType2Quality; + if (qualityDifference < 0) + { + return -1; + } + else if (qualityDifference > 0) + { + return 1; + } + + return 0; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ParsedMediaTypeHeaderValue.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ParsedMediaTypeHeaderValue.cs new file mode 100644 index 0000000000..d0b966789b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/ParsedMediaTypeHeaderValue.cs @@ -0,0 +1,75 @@ +// 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 ASPNETCORE50 + +using System.Diagnostics.Contracts; +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + // This type is instanciated by frequently called comparison methods so is very performance sensitive + internal struct ParsedMediaTypeHeaderValue + { + private const char MediaRangeAsterisk = '*'; + private const char MediaTypeSubtypeDelimiter = '/'; + + private readonly string _mediaType; + private readonly int _delimiterIndex; + private readonly bool _isAllMediaRange; + private readonly bool _isSubtypeMediaRange; + + public ParsedMediaTypeHeaderValue(MediaTypeHeaderValue mediaTypeHeaderValue) + { + Contract.Assert(mediaTypeHeaderValue != null); + string mediaType = _mediaType = mediaTypeHeaderValue.MediaType; + _delimiterIndex = mediaType.IndexOf(MediaTypeSubtypeDelimiter); + Contract.Assert(_delimiterIndex > 0, "The constructor of the MediaTypeHeaderValue would have failed if there wasn't a type and subtype."); + + _isAllMediaRange = false; + _isSubtypeMediaRange = false; + int mediaTypeLength = mediaType.Length; + if (_delimiterIndex == mediaTypeLength - 2) + { + if (mediaType[mediaTypeLength - 1] == MediaRangeAsterisk) + { + _isSubtypeMediaRange = true; + if (_delimiterIndex == 1 && mediaType[0] == MediaRangeAsterisk) + { + _isAllMediaRange = true; + } + } + } + } + + public bool IsAllMediaRange + { + get { return _isAllMediaRange; } + } + + public bool IsSubtypeMediaRange + { + get { return _isSubtypeMediaRange; } + } + + public bool TypesEqual(ref ParsedMediaTypeHeaderValue other) + { + if (_delimiterIndex != other._delimiterIndex) + { + return false; + } + return String.Compare(_mediaType, 0, other._mediaType, 0, _delimiterIndex, StringComparison.OrdinalIgnoreCase) == 0; + } + + public bool SubTypesEqual(ref ParsedMediaTypeHeaderValue other) + { + int _subTypeLength = _mediaType.Length - _delimiterIndex - 1; + if (_subTypeLength != other._mediaType.Length - other._delimiterIndex - 1) + { + return false; + } + return String.Compare(_mediaType, _delimiterIndex + 1, other._mediaType, other._delimiterIndex + 1, _subTypeLength, StringComparison.OrdinalIgnoreCase) == 0; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/StringWithQualityHeaderValueComparer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/StringWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000..89a84a3a0e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ContentNegotiator/StringWithQualityHeaderValueComparer.cs @@ -0,0 +1,77 @@ +// 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 ASPNETCORE50 + +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// + /// 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(StringWithQualityHeaderValue stringWithQuality1, + StringWithQualityHeaderValue stringWithQuality2) + { + Contract.Assert(stringWithQuality1 != null); + Contract.Assert(stringWithQuality2 != null); + + double quality1 = stringWithQuality1.Quality ?? FormattingUtilities.Match; + double quality2 = stringWithQuality2.Quality ?? FormattingUtilities.Match; + double 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; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs index bce4ef5c99..8deae66a1e 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs @@ -20,6 +20,9 @@ namespace System.Net.Http /// public static class HttpRequestMessageExtensions { + +#if !ASPNETCORE50 + /// /// Helper method for creating an message with a "416 (Requested Range Not Satisfiable)" status code. /// This response can be used in combination with the to indicate that the requested range or @@ -41,6 +44,8 @@ namespace System.Net.Http return rangeNotSatisfiableResponse; } +#endif + /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an with message . diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json index 12a0f20de6..2f729fbe7a 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json @@ -1,7 +1,7 @@ { "version": "6.0.0-*", "compilationOptions": { - "warningsAsErrors": true + "warningsAsErrors": false }, "dependencies": { "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, @@ -13,6 +13,11 @@ "frameworkAssemblies": { "System.Net.Http": "4.0.0.0" } + }, + "aspnetcore50": { + "dependencies": { + "System.Net.Http": "4.0.0-beta-*" + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs index 93072ae413..719e4d3b46 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs @@ -1,7 +1,6 @@ // 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 ASPNET50 using System; using System.Net; using System.Net.Http; @@ -623,5 +622,4 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests public string ControllerName { get; set; } } } -} - #endif \ No newline at end of file +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index 1bf126c9ff..eef6773203 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -1,7 +1,6 @@ // 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 ASPNET50 using System; using System.Collections.Generic; using System.Net; @@ -58,6 +57,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests content); } +#if !ASPNETCORE50 + [Fact] public async Task Options_SetsDefaultFormatters() { @@ -83,6 +84,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expected, formatters); } +#endif + [Fact] public async Task ActionThrowsHttpResponseException_WithStatusCode() { @@ -521,4 +524,3 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } } } -#endif diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 72b46b1642..f1e68382fc 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -24,7 +24,8 @@ "ValueProvidersSite": "1.0.0", "XmlSerializerWebSite": "1.0.0", "UrlHelperWebSite": "1.0.0", - + "WebApiCompatShimWebSite": "1.0.0", + "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", @@ -33,7 +34,6 @@ "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.Logging": "1.0.0-*", "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", "TagHelpersWebSite": "1.0.0", "Xunit.KRunner": "1.0.0-*" }, @@ -43,8 +43,7 @@ "frameworks": { "aspnet50": { "dependencies": { - "AutofacWebSite": "1.0.0", - "WebApiCompatShimWebSite": "1.0.0" + "AutofacWebSite": "1.0.0" } }, "aspnetcore50": { diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index 3d96a3b3d3..58bde7f949 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -1,6 +1,8 @@ // 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 !ASPNETCORE50 + using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -344,4 +346,5 @@ namespace System.Web.Http.TestControllers return null; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/DefaultContentNegotiatorTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/DefaultContentNegotiatorTest.cs new file mode 100644 index 0000000000..2427d659d2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/DefaultContentNegotiatorTest.cs @@ -0,0 +1,835 @@ +// 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 System.Linq; +using System.Net.Http.Formatting.Mocks; +using System.Net.Http.Headers; +using System.Text; +using Newtonsoft.Json.Linq; +using Xunit; +using Microsoft.TestCommon; + +namespace System.Net.Http.Formatting +{ + public class DefaultContentNegotiatorTests + { + private readonly DefaultContentNegotiator _negotiator = new DefaultContentNegotiator(); + private readonly HttpRequestMessage _request = new HttpRequestMessage(); + + public static TheoryData MatchRequestMediaTypeData + { + get + { + // string requestMediaType, string[] supportedMediaTypes, string expectedMediaType + return new TheoryData + { + { "text/plain", new string[0], null }, + { "text/plain", new string[] { "text/xml", "application/xml" }, null }, + { "application/xml", new string[] { "application/xml", "text/xml" }, "application/xml" }, + { "APPLICATION/XML", new string[] { "text/xml", "application/xml" }, "application/xml" }, + { "application/xml; charset=utf-8", new string[] { "text/xml", "application/xml" }, "application/xml" }, + { "application/xml; charset=utf-8; parameter=value", new string[] { "text/xml", "application/xml" }, "application/xml" }, + }; + } + } + + public static TheoryData MatchAcceptHeaderData + { + get + { + // string[] acceptHeader, string[] supportedMediaTypes, string expectedMediaType, double matchQuality, int range + return new TheoryData + { + { new string[] { "text/plain" }, new string[0], null, 0.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + + { new string[] { "text/plain" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + { new string[] { "text/plain; q=0.5" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + + { new string[] { "application/xml" }, new string[] { "application/xml", "text/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "APPLICATION/XML; q=0.5" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "text/xml; q=0.5", "APPLICATION/XML; q=0.7" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.7, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "application/xml; q=0.0" }, new string[] { "application/xml", "text/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + { new string[] { "APPLICATION/XML; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + { new string[] { "text/xml; q=0.0", "APPLICATION/XML; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + + { new string[] { "text/*" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange }, + { new string[] { "text/*", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "text/*", "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange }, + { new string[] { "text/*; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange }, + { new string[] { "text/*; q=0.5", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "text/*; q=0.0", "application/xml; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + { new string[] { "text/*; q=0.0", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + + { new string[] { "*/*; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange }, + { new string[] { "*/*; q=0.0" }, new string[] { "text/xml", "application/xml" }, null, 0.0, (int)MediaTypeFormatterMatchRanking.None }, + { new string[] { "*/*; q=0.5", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + { new string[] { "*/*; q=1.0", "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "text/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange }, + { new string[] { "*/*", "application/xml" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + + { new string[] { "text/*; q=0.5", "*/*; q=0.2", "application/xml; q=1.0" }, new string[] { "text/xml", "application/xml" }, "application/xml", 1.0, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + + { new string[] { "application/xml; q=0.5" }, new string[] { "text/xml", "application/xml" }, "application/xml", 0.5, (int)MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral }, + }; + } + } + + public static TheoryData ShouldMatchOnTypeData + { + get + { + // bool excludeMatchOnType, string[] acceptHeaders, bool expectedResult + return new TheoryData + { + { false, new string[0], true }, + { true, new string[0], true }, + + { false, new string[] { "application/xml" }, true }, + { true, new string[] { "application/xml" }, false }, + + { false, new string[] { "application/xml; q=1.0" }, true }, + { true, new string[] { "application/xml; q=1.0" }, false }, + + { false, new string[] { "application/xml; q=0.0" }, true }, + { true, new string[] { "application/xml; q=0.0" }, false }, + + { false, new string[] { "application/xml; q=0.0", "application/json" }, true }, + { true, new string[] { "application/xml; q=0.0", "application/json" }, false }, + + { false, new string[] { "text/nomatch" }, true }, + { true, new string[] { "text/nomatch" }, false }, + }; + } + } + + public static TheoryData MatchTypeData + { + get + { + // string[] supportedMediaTypes, string expectedMediaType + return new TheoryData + { + { new string[0], "application/octet-stream" }, + + { new string[] { "text/xml", "application/xml" }, "text/xml" }, + { new string[] { "application/xml", "text/xml" }, "application/xml" }, + }; + } + } + + public static TheoryData SelectResponseCharacterEncodingData + { + get + { + // string[] acceptEncodings, string requestEncoding, string[] supportedEncodings, string expectedEncoding + return new TheoryData + { + { new string[] { "utf-8" }, null, new string[0], null }, + { new string[0], "utf-8", new string[0], null }, + + { new string[0], null, new string[] { "utf-8", "utf-16"}, "utf-8" }, + { new string[0], "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" }, + + { new string[] { "utf-8" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" }, + { new string[] { "utf-16" }, "utf-8", new string[] { "utf-8", "utf-16"}, "utf-16" }, + { new string[] { "utf-16; q=0.5" }, "utf-8", new string[] { "utf-8", "utf-16"}, "utf-16" }, + + { new string[] { "utf-8; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" }, + { new string[] { "utf-8; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" }, + { new string[] { "utf-8; q=0.0", "utf-16; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" }, + { new string[] { "utf-8; q=0.0", "utf-16; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" }, + { new string[] { "*; q=0.0" }, null, new string[] { "utf-8", "utf-16"}, "utf-8" }, + { new string[] { "*; q=0.0" }, "utf-16", new string[] { "utf-8", "utf-16"}, "utf-16" }, + }; + } + } + + public static TheoryData, MediaTypeFormatterMatch> SelectResponseMediaTypeData + { + get + { +#if !ASPNETCORE50 + // Only mapping and accept makes sense with q != 1.0 + MediaTypeFormatterMatch matchMapping10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping); + MediaTypeFormatterMatch matchMapping05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping); +#endif + + MediaTypeFormatterMatch matchAccept10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral); + MediaTypeFormatterMatch matchAccept05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral); + + MediaTypeFormatterMatch matchAcceptSubTypeRange10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange); + MediaTypeFormatterMatch matchAcceptSubTypeRange05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange); + + MediaTypeFormatterMatch matchAcceptAllRange10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange); + MediaTypeFormatterMatch matchAcceptAllRange05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange); + + MediaTypeFormatterMatch matchRequest10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnRequestMediaType); + MediaTypeFormatterMatch matchType10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.MatchOnCanWriteType); + + // ICollection candidateMatches, MediaTypeFormatterMatch winner + return new TheoryData, MediaTypeFormatterMatch> + { + { new List(), null }, + { new List() { matchType10 }, matchType10 }, + { new List() { matchType10, matchRequest10 }, matchRequest10 }, + { new List() { matchType10, matchRequest10, matchAcceptAllRange10 }, matchAcceptAllRange10 }, + { new List() { matchType10, matchRequest10, matchAcceptAllRange10, matchAcceptSubTypeRange10 }, matchAcceptSubTypeRange10 }, + { new List() { matchType10, matchRequest10, matchAcceptAllRange10, matchAcceptSubTypeRange10, matchAccept10 }, matchAccept10 }, +#if !ASPNETCORE50 + { new List() { matchType10, matchRequest10, matchAcceptAllRange10, matchAcceptSubTypeRange10, matchAccept10, matchMapping10 }, matchMapping10 }, +#endif + { new List() { matchAccept05, matchAccept10 }, matchAccept10 }, + { new List() { matchAccept10, matchAccept05 }, matchAccept10 }, + + { new List() { matchAcceptSubTypeRange05, matchAcceptSubTypeRange10 }, matchAcceptSubTypeRange10 }, + { new List() { matchAcceptSubTypeRange10, matchAcceptSubTypeRange05 }, matchAcceptSubTypeRange10 }, + + { new List() { matchAcceptAllRange05, matchAcceptAllRange10 }, matchAcceptAllRange10 }, + { new List() { matchAcceptAllRange10, matchAcceptAllRange05 }, matchAcceptAllRange10 }, +#if !ASPNETCORE50 + { new List() { matchMapping05, matchMapping10 }, matchMapping10 }, + { new List() { matchMapping10, matchMapping05 }, matchMapping10 }, + + { new List() { matchMapping05, matchAccept05 }, matchMapping05 }, + { new List() { matchMapping10, matchAccept10 }, matchMapping10 }, + + { new List() { matchMapping05, matchAcceptSubTypeRange05 }, matchMapping05 }, + { new List() { matchMapping10, matchAcceptSubTypeRange10 }, matchMapping10 }, + + { new List() { matchMapping05, matchAcceptAllRange05 }, matchMapping05 }, + { new List() { matchMapping10, matchAcceptAllRange10 }, matchMapping10 }, + + { new List() { matchMapping05, matchAccept10 }, matchAccept10 }, + { new List() { matchMapping05, matchAcceptSubTypeRange10 }, matchAcceptSubTypeRange10 }, + { new List() { matchMapping05, matchAcceptAllRange10 }, matchAcceptAllRange10 }, +#endif + }; + } + } + + public static TheoryData UpdateBestMatchData + { + get + { + MediaTypeFormatterMatch matchMapping10 = CreateMatch(1.0, MediaTypeFormatterMatchRanking.None); + MediaTypeFormatterMatch matchMapping05 = CreateMatch(0.5, MediaTypeFormatterMatchRanking.None); + + // MediaTypeFormatterMatch current, MediaTypeFormatterMatch potentialReplacement, currentWins + return new TheoryData + { + { null, matchMapping10, false }, + { null, matchMapping05, false }, + + { matchMapping10, matchMapping10, true }, + { matchMapping10, matchMapping05, true }, + + { matchMapping05, matchMapping10, false }, + { matchMapping05, matchMapping05, true }, + }; + } + } + + private static MediaTypeFormatterMatch CreateMatch(double? quality, MediaTypeFormatterMatchRanking ranking) + { + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter(); + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/test"); + return new MediaTypeFormatterMatch(formatter, mediaType, quality, ranking); + } + + [Fact] + public void TypeIsCorrect() + { + new TypeAssert().HasProperties(typeof(DefaultContentNegotiator), TypeAssert.TypeProperties.IsPublicVisibleClass); + } + + [Fact] + public void Negotiate_ForEmptyFormatterCollection_ReturnsNull() + { + var result = _negotiator.Negotiate(typeof(string), _request, Enumerable.Empty()); + + Assert.Null(result); + } + +#if !ASPNETCORE50 + + [Fact] + public void Negotiate_MediaTypeMappingTakesPrecedenceOverAcceptHeader() + { + // Prepare the request message + _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _request.Headers.Add("Browser", "IE"); + _request.Headers.Add("Cookie", "ABC"); + + // Prepare the formatters + List formatters = new List(); + formatters.Add(new JsonMediaTypeFormatter()); + formatters.Add(new XmlMediaTypeFormatter()); + PlainTextFormatter frmtr = new PlainTextFormatter(); + frmtr.SupportedMediaTypes.Clear(); + frmtr.MediaTypeMappings.Clear(); + frmtr.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml")); + frmtr.MediaTypeMappings.Add(new MyMediaTypeMapping(new MediaTypeHeaderValue(("application/xml")))); + formatters.Add(frmtr); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, formatters); + + // Assert + Assert.NotNull(result); + Assert.Equal("application/xml", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + +#endif + + [Fact] + public void Negotiate_ForRequestReturnsFirstMatchingFormatter() + { + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/myMediaType"); + + MediaTypeFormatter formatter1 = new MockMediaTypeFormatter() + { + CanWriteTypeCallback = (Type t) => false + }; + + MediaTypeFormatter formatter2 = new MockMediaTypeFormatter() + { + CanWriteTypeCallback = (Type t) => true + }; + + formatter2.SupportedMediaTypes.Add(mediaType); + + MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection( + new MediaTypeFormatter[] + { + formatter1, + formatter2 + }); + + _request.Content = new StringContent("test", Encoding.UTF8, mediaType.MediaType); + + var result = _negotiator.Negotiate(typeof(string), _request, collection); + Assert.Same(formatter2, result.Formatter); + new MediaTypeAssert().AreEqual(mediaType, result.MediaType, "Expected the formatter's media type to be returned."); + } + + [Fact] + public void Negotiate_SelectsJsonAsDefaultFormatter() + { + // Arrange + _request.Content = new StringContent("test"); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + Assert.IsType(result.Formatter); + Assert.Equal(MediaTypeConstants.ApplicationJsonMediaType.MediaType, result.MediaType.MediaType); + } + + [Fact] + public void Negotiate_SelectsXmlFormatter_ForXhrRequestThatAcceptsXml() + { + // Arrange + _request.Content = new StringContent("test"); + _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + Assert.Equal("application/xml", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + + [Fact] + public void Negotiate_SelectsJsonFormatter_ForXhrRequestThatDoesNotSpecifyAcceptHeaders() + { + // Arrange + _request.Content = new StringContent("test"); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + Assert.Equal("application/json", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + +#if !ASPNETCORE50 + + [Fact] + public void Negotiate_RespectsFormatterOrdering_ForXhrRequestThatDoesNotSpecifyAcceptHeaders() + { + // Arrange + _request.Content = new StringContent("test"); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + + MediaTypeFormatterCollection formatters = new MediaTypeFormatterCollection(new MediaTypeFormatter[] + { + new XmlMediaTypeFormatter(), + new JsonMediaTypeFormatter(), + new FormUrlEncodedMediaTypeFormatter() + }); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, formatters); + + // Assert + Assert.Equal("application/json", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + +#endif + + [Fact] + public void Negotiate_SelectsJsonFormatter_ForXHRAndJsonValueResponse() + { + // Arrange + _request.Content = new StringContent("test"); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + + // Act + var result = _negotiator.Negotiate(typeof(JToken), _request, new MediaTypeFormatterCollection()); + + Assert.Equal("application/json", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + + [Fact] + public void Negotiate_SelectsJsonFormatter_ForXHRAndMatchAllAcceptHeader() + { + // Accept + _request.Content = new StringContent("test"); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + Assert.Equal("application/json", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + + [Fact] + public void Negotiate_UsesRequestedFormatterForXHRAndMatchAllPlusOtherAcceptHeader() + { + // Arrange + _request.Content = new StringContent("test"); + _request.Headers.Add("x-requested-with", "XMLHttpRequest"); + _request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); // XHR header sent by Firefox 3b5 + + // Act + var result = _negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + Assert.Equal("application/xml", result.MediaType.MediaType); + Assert.IsType(result.Formatter); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Negotiate_ObservesExcludeMatchOnTypeOnly(bool excludeMatchOnTypeOnly) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(excludeMatchOnTypeOnly); + _request.Content = new StringContent("test"); + _request.Headers.Accept.ParseAdd("text/html"); + + // Act + var result = negotiator.Negotiate(typeof(string), _request, new MediaTypeFormatterCollection()); + + // Assert + if (excludeMatchOnTypeOnly) + { + Assert.Null(result); + } + else + { + Assert.NotNull(result); + Assert.Equal("application/json", result.MediaType.MediaType); + } + } + +#if !ASPNETCORE50 + + [Fact] + public void MatchMediaTypeMapping_ReturnsMatch() + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + HttpRequestMessage request = new HttpRequestMessage(); + MediaTypeHeaderValue mappingMediatype = MediaTypeHeaderValue.Parse("application/other"); + MockMediaTypeMapping mockMediaTypeMapping = new MockMediaTypeMapping(mappingMediatype, 0.75); + + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter(); + formatter.MediaTypeMappings.Add(mockMediaTypeMapping); + + // Act + MediaTypeFormatterMatch match = negotiator.MatchMediaTypeMapping(request, formatter); + + // Assert + Assert.True(mockMediaTypeMapping.WasInvoked); + Assert.Same(request, mockMediaTypeMapping.Request); + + Assert.Same(formatter, match.Formatter); + Assert.Equal(mockMediaTypeMapping.MediaType, match.MediaType); + Assert.Equal(mockMediaTypeMapping.MatchQuality, match.Quality); + Assert.Equal(MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping, match.Ranking); + } + +#endif + + [Theory] + [MemberData("MatchAcceptHeaderData")] + public void MatchAcceptHeader_ReturnsMatch(string[] acceptHeaders, string[] supportedMediaTypes, string expectedMediaType, double expectedQuality, int ranking) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + List unsortedAcceptHeaders = acceptHeaders.Select(a => MediaTypeWithQualityHeaderValue.Parse(a)).ToList(); + IEnumerable sortedAcceptHeaders = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedAcceptHeaders); + + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter(); + foreach (string supportedMediaType in supportedMediaTypes) + { + formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType)); + } + + // Act + MediaTypeFormatterMatch match = negotiator.MatchAcceptHeader(sortedAcceptHeaders, formatter); + + // Assert + if (expectedMediaType == null) + { + Assert.Null(match); + } + else + { + Assert.Same(formatter, match.Formatter); + Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType); + Assert.Equal(expectedQuality, match.Quality); + Assert.Equal(ranking, (int)match.Ranking); + } + } + + [Theory] + [MemberData("MatchRequestMediaTypeData")] + public void MatchRequestMediaType_ReturnsMatch(string requestMediaType, string[] supportedMediaTypes, string expectedMediaType) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + HttpRequestMessage request = new HttpRequestMessage(); + request.Content = new StringContent(String.Empty); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(requestMediaType); + + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter(); + foreach (string supportedMediaType in supportedMediaTypes) + { + formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType)); + } + + // Act + MediaTypeFormatterMatch match = negotiator.MatchRequestMediaType(request, formatter); + + // Assert + if (expectedMediaType == null) + { + Assert.Null(match); + } + else + { + Assert.Same(formatter, match.Formatter); + Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType); + Assert.Equal(1.0, match.Quality); + Assert.Equal(MediaTypeFormatterMatchRanking.MatchOnRequestMediaType, match.Ranking); + } + } + + [Theory] + [MemberData("ShouldMatchOnTypeData")] + public void ShouldMatchOnType_ReturnsExpectedResult(bool excludeMatchOnType, string[] acceptHeaders, bool expectedResult) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(excludeMatchOnType); + List unsortedAcceptHeaders = acceptHeaders.Select(a => MediaTypeWithQualityHeaderValue.Parse(a)).ToList(); + IEnumerable sortedAcceptHeaders = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedAcceptHeaders); + + // Act + bool result = negotiator.ShouldMatchOnType(sortedAcceptHeaders); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData("MatchTypeData")] + public void MatchType_ReturnsMatch(string[] supportedMediaTypes, string expectedMediaType) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter(); + foreach (string supportedMediaType in supportedMediaTypes) + { + formatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(supportedMediaType)); + } + + // Act + MediaTypeFormatterMatch match = negotiator.MatchType(typeof(object), formatter); + + // Assert + Assert.Same(formatter, match.Formatter); + Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), match.MediaType); + Assert.Equal(1.0, match.Quality); + Assert.Equal(MediaTypeFormatterMatchRanking.MatchOnCanWriteType, match.Ranking); + } + + [Theory] + [MemberData("SelectResponseMediaTypeData")] + public void SelectResponseMediaTypeFormatter_SelectsMediaType(ICollection matches, MediaTypeFormatterMatch expectedWinner) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + // Act + MediaTypeFormatterMatch actualWinner = negotiator.SelectResponseMediaTypeFormatter(matches); + + // Assert + Assert.Same(expectedWinner, actualWinner); + } + + [Theory] + [MemberData("SelectResponseCharacterEncodingData")] + public void SelectResponseCharacterEncoding_SelectsEncoding(string[] acceptCharsetHeaders, string requestEncoding, string[] supportedEncodings, string expectedEncoding) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + HttpRequestMessage request = new HttpRequestMessage(); + foreach (string acceptCharsetHeader in acceptCharsetHeaders) + { + request.Headers.AcceptCharset.Add(StringWithQualityHeaderValue.Parse(acceptCharsetHeader)); + } + + if (requestEncoding != null) + { + Encoding reqEncoding = Encoding.GetEncoding(requestEncoding); + StringContent content = new StringContent("", reqEncoding, "text/plain"); + request.Content = content; + } + + MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true }; + foreach (string supportedEncoding in supportedEncodings) + { + formatter.SupportedEncodings.Add(Encoding.GetEncoding(supportedEncoding)); + } + + // Act + Encoding actualEncoding = negotiator.SelectResponseCharacterEncoding(request, formatter); + + // Assert + if (expectedEncoding == null) + { + Assert.Null(actualEncoding); + } + else + { + Assert.Equal(Encoding.GetEncoding(expectedEncoding), actualEncoding); + } + } + + [Theory] + [TestDataSet(typeof(DefaultContentNegotiatorTests), nameof(MediaTypeWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))] + public void SortMediaTypeWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + List unsortedValues = + new List(unsorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u))); + + List expectedSortedValues = + new List(expectedSorted.Select(u => MediaTypeWithQualityHeaderValue.Parse(u))); + + // Act + IEnumerable actualSorted = negotiator.SortMediaTypeWithQualityHeaderValuesByQFactor(unsortedValues); + + // Assert + Assert.True(expectedSortedValues.SequenceEqual(actualSorted)); + } + + public static TheoryData MediaTypeWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + { + get + { + return new TheoryData + { + { + 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] + [TestDataSet(typeof(DefaultContentNegotiatorTests), nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))] + public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + List unsortedValues = + new List(unsorted.Select(u => StringWithQualityHeaderValue.Parse(u))); + + List expectedSortedValues = + new List(expectedSorted.Select(u => StringWithQualityHeaderValue.Parse(u))); + + // Act + IEnumerable actualSorted = negotiator.SortStringWithQualityHeaderValuesByQFactor(unsortedValues); + + // Assert + Assert.True(expectedSortedValues.SequenceEqual(actualSorted)); + } + + public static TheoryData StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + { + get + { + return new TheoryData + { + { + new string[] + { + "text", + "text;q=1.0", + "text", + "text;q=0", + "*;q=0.8", + "*;q=1", + "text;q=0.8", + "*;q=0.6", + "text;q=1.0", + "*;q=0.4", + "text;q=0.6", + }, + new string[] + { + "text", + "text;q=1.0", + "text", + "text;q=1.0", + "*;q=1", + "text;q=0.8", + "*;q=0.8", + "text;q=0.6", + "*;q=0.6", + "*;q=0.4", + "text;q=0", + } + } + }; + } + } + + [Theory] + [MemberData("UpdateBestMatchData")] + public void UpdateBestMatch_SelectsCorrectly(MediaTypeFormatterMatch current, MediaTypeFormatterMatch replacement, bool currentWins) + { + // Arrange + MockContentNegotiator negotiator = new MockContentNegotiator(); + + // Act + MediaTypeFormatterMatch actualResult = negotiator.UpdateBestMatch(current, replacement); + + // Assert + if (currentWins) + { + Assert.Same(current, actualResult); + } + else + { + Assert.Same(replacement, actualResult); + } + } + + private class PlainTextFormatter : MediaTypeFormatter + { + public override bool CanReadType(Type type) + { + return true; + } + + public override bool CanWriteType(Type type) + { + return true; + } + } + +#if !ASPNETCORE50 + + private class MyMediaTypeMapping : MediaTypeMapping + { + public MyMediaTypeMapping(MediaTypeHeaderValue mediaType) + : base(mediaType) + { + } + + public override double TryMatchMediaType(HttpRequestMessage request) + { + if (request.Headers.Contains("Cookie")) + { + return 1.0; + } + else + { + return 0; + } + } + } + +#endif + + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs index 3a30640193..74a25ead29 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs @@ -108,6 +108,8 @@ namespace System.Web.Http.Dispatcher Assert.DoesNotContain("OH NO", modelStateError["[2].Name"] as IEnumerable); } +#if !ASPNETCORE50 + [Fact] public void HttpError_Roundtrips_WithJsonFormatter() { @@ -196,6 +198,8 @@ namespace System.Web.Http.Dispatcher serializedError); } +#endif + [Fact] public void GetPropertyValue_GetsValue_IfTypeMatches() { diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs index 0e4d71c009..37ea900b0a 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs @@ -8,13 +8,46 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.WebApiCompatShim; using Microsoft.AspNet.PipelineCore; using Microsoft.Framework.OptionsModel; +#if !ASPNETCORE50 using Moq; +#endif using Xunit; namespace System.Net.Http { public class HttpRequestMessageExtensionsTest { + [Fact] + public void CreateResponse_MatchingMediaType_WhenMediaTypeStringIsInvalidFormat_Throws() + { + HttpRequestMessage request = CreateRequest(new DefaultHttpContext()); + + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), "foo/bar; param=value")); + + Assert.Equal("The format of value 'foo/bar; param=value' is invalid.", ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_WhenRequestDoesNotHaveHttpContextThrows() + { + HttpRequestMessage request = CreateRequest(null); + + // Arrange + + // Act + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); + + Assert.Equal( + "The HttpRequestMessage instance is not properly initialized. " + + "Use HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage to create an HttpRequestMessage " + + "for the current request.", + ex.Message); + } + +#if !ASPNETCORE50 + [Fact] public void CreateResponse_DoingConneg_OnlyContent_RetrievesContentNegotiatorFromServices() { @@ -110,36 +143,6 @@ namespace System.Net.Http Assert.Same(formatter, objectContent.Formatter); } - - [Fact] - public void CreateResponse_MatchingMediaType_WhenMediaTypeStringIsInvalidFormat_Throws() - { - HttpRequestMessage request = CreateRequest(new DefaultHttpContext()); - - var ex = Assert.Throws( - () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), "foo/bar; param=value")); - - Assert.Equal("The format of value 'foo/bar; param=value' is invalid.", ex.Message); - } - - [Fact] - public void CreateResponse_MatchingMediaType_WhenRequestDoesNotHaveHttpContextThrows() - { - HttpRequestMessage request = CreateRequest(null); - - // Arrange - - // Act - var ex = Assert.Throws( - () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); - - Assert.Equal( - "The HttpRequestMessage instance is not properly initialized. " + - "Use HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage to create an HttpRequestMessage " + - "for the current request.", - ex.Message); - } - [Fact] public void CreateResponse_MatchingMediaType_WhenMediaTypeDoesNotMatch_Throws() { @@ -299,20 +302,8 @@ namespace System.Net.Http Assert.Same(expectedContentRange, response.Content.Headers.ContentRange); } - private static HttpRequestMessage CreateRequest(HttpContext context) - { - var request = new HttpRequestMessage(); - request.Properties.Add(nameof(HttpContext), context); - return request; - } - - private static object CreateValue() - { - return new object(); - } - private static IServiceProvider CreateServices( - IContentNegotiator contentNegotiator = null, + IContentNegotiator contentNegotiator = null, MediaTypeFormatter formatter = null) { var options = new WebApiCompatShimOptions(); @@ -343,5 +334,17 @@ namespace System.Net.Http return services.Object; } +#endif + private static object CreateValue() + { + return new object(); + } + + private static HttpRequestMessage CreateRequest(HttpContext context) + { + var request = new HttpRequestMessage(); + request.Properties.Add(nameof(HttpContext), context); + return request; + } } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs index b03db244ef..0fc6689f64 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs @@ -6,13 +6,28 @@ using System.Net; using System.Net.Http; using Microsoft.AspNet.PipelineCore; using Microsoft.AspNet.Routing; +#if !ASPNETCORE50 using Moq; +#endif using Xunit; namespace Microsoft.AspNet.Mvc.WebApiCompatShim { public class HttpResponseExceptionActionFilterTest { + [Fact] + public void OrderIsSetToMaxValue() + { + // Arrange + var filter = new HttpResponseExceptionActionFilter(); + var expectedFilterOrder = int.MaxValue - 10; + + // Act & Assert + Assert.Equal(expectedFilterOrder, filter.Order); + } + +#if !ASPNETCORE50 + [Fact] public void OnActionExecuting_IsNoOp() { @@ -32,17 +47,6 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim Assert.Null(context.Result); } - [Fact] - public void OrderIsSetToMaxValue() - { - // Arrange - var filter = new HttpResponseExceptionActionFilter(); - var expectedFilterOrder = int.MaxValue - 10; - - // Act & Assert - Assert.Equal(expectedFilterOrder, filter.Order); - } - [Fact] public void OnActionExecuted_HandlesExceptionAndReturnsObjectResult() { @@ -71,5 +75,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim Assert.Equal(context.HttpContext.GetHttpRequestMessage(), response.RequestMessage); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + +#endif + } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs index 4c241ebf14..c90664d31c 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs @@ -1,6 +1,8 @@ // 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 !ASPNETCORE50 + using System; using System.IO; using System.Net.Http; @@ -40,7 +42,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShimTest Object = outputValue, DeclaredType = outputType, ActionContext = new ActionContext(new DefaultHttpContext(), routeData: null, actionDescriptor: null) - }; + }; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockContentNegotiator.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockContentNegotiator.cs new file mode 100644 index 0000000000..a959c59e75 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockContentNegotiator.cs @@ -0,0 +1,81 @@ +// 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 System.Collections.ObjectModel; +using System.Net.Http.Headers; +using System.Text; + +namespace System.Net.Http.Formatting.Mocks +{ + public class MockContentNegotiator : DefaultContentNegotiator + { + public MockContentNegotiator() + { + } + + public MockContentNegotiator(bool excludeMatchOnTypeOnly) + : base(excludeMatchOnTypeOnly) + { + } + + public new Collection ComputeFormatterMatches(Type type, HttpRequestMessage request, IEnumerable formatters) + { + return base.ComputeFormatterMatches(type, request, formatters); + } + + public new MediaTypeFormatterMatch SelectResponseMediaTypeFormatter(ICollection matches) + { + return base.SelectResponseMediaTypeFormatter(matches); + } + + public new Encoding SelectResponseCharacterEncoding(HttpRequestMessage request, MediaTypeFormatter formatter) + { + return base.SelectResponseCharacterEncoding(request, formatter); + } + +#if !ASPNETCORE50 + + public new MediaTypeFormatterMatch MatchMediaTypeMapping(HttpRequestMessage request, MediaTypeFormatter formatter) + { + return base.MatchMediaTypeMapping(request, formatter); + } + +#endif + + public new MediaTypeFormatterMatch MatchAcceptHeader(IEnumerable sortedAcceptValues, MediaTypeFormatter formatter) + { + return base.MatchAcceptHeader(sortedAcceptValues, formatter); + } + + public new MediaTypeFormatterMatch MatchRequestMediaType(HttpRequestMessage request, MediaTypeFormatter formatter) + { + return base.MatchRequestMediaType(request, formatter); + } + + public new bool ShouldMatchOnType(IEnumerable sortedAcceptValues) + { + return base.ShouldMatchOnType(sortedAcceptValues); + } + + public new MediaTypeFormatterMatch MatchType(Type type, MediaTypeFormatter formatter) + { + return base.MatchType(type, formatter); + } + + public new IEnumerable SortMediaTypeWithQualityHeaderValuesByQFactor(ICollection headerValues) + { + return base.SortMediaTypeWithQualityHeaderValuesByQFactor(headerValues); + } + + public new IEnumerable SortStringWithQualityHeaderValuesByQFactor(ICollection headerValues) + { + return base.SortStringWithQualityHeaderValuesByQFactor(headerValues); + } + + public new MediaTypeFormatterMatch UpdateBestMatch(MediaTypeFormatterMatch current, MediaTypeFormatterMatch potentialReplacement) + { + return base.UpdateBestMatch(current, potentialReplacement); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeFormatter.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeFormatter.cs new file mode 100644 index 0000000000..6fa5c9f5f4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeFormatter.cs @@ -0,0 +1,47 @@ +// 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.Net.Http.Headers; +using System.Text; + +namespace System.Net.Http.Formatting.Mocks +{ + public class MockMediaTypeFormatter : MediaTypeFormatter + { + private bool _canWriteAnyTypes = true; + public bool CallBase { get; set; } + public Func CanReadTypeCallback { get; set; } + public Func CanWriteTypeCallback { get; set; } + + public bool CanWriteAnyTypesReturn + { + get { return _canWriteAnyTypes; } + set { _canWriteAnyTypes = value; } + } + + public override bool CanReadType(Type type) + { + if (!CallBase && CanReadTypeCallback == null) + { + throw new InvalidOperationException("CallBase or CanReadTypeCallback must be set first."); + } + + return CanReadTypeCallback != null ? CanReadTypeCallback(type) : true; + } + + public override bool CanWriteType(Type type) + { + if (!CallBase && CanWriteTypeCallback == null) + { + throw new InvalidOperationException("CallBase or CanWriteTypeCallback must be set first."); + } + + return CanWriteTypeCallback != null ? CanWriteTypeCallback(type) : true; + } + + public new Encoding SelectCharacterEncoding(HttpContentHeaders contentHeaders) + { + return base.SelectCharacterEncoding(contentHeaders); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeMapping.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeMapping.cs new file mode 100644 index 0000000000..7fde707f3e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Mocks/MockMediaTypeMapping.cs @@ -0,0 +1,37 @@ +// 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 !ASPNETCORE50 + +using System.Net.Http.Headers; +namespace System.Net.Http.Formatting.Mocks +{ + public class MockMediaTypeMapping : MediaTypeMapping + { + public MockMediaTypeMapping(string mediaType, double matchQuality) + : base(mediaType) + { + MatchQuality = matchQuality; + } + + public MockMediaTypeMapping(MediaTypeHeaderValue mediaType, double matchQuality) + : base(mediaType) + { + MatchQuality = matchQuality; + } + + public double MatchQuality { get; private set; } + + public HttpRequestMessage Request { get; private set; } + + public bool WasInvoked { get; private set; } + + public override double TryMatchMediaType(HttpRequestMessage request) + { + WasInvoked = true; + Request = request; + return MatchQuality; + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/FlagsEnum.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/FlagsEnum.cs new file mode 100644 index 0000000000..ad24d873e0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/FlagsEnum.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Microsoft.TestCommon.Types +{ + [Flags] + public enum FlagsEnum + { + One = 0x1, + Two = 0x2, + Four = 0x4 + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/LongEnum.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/LongEnum.cs new file mode 100644 index 0000000000..05a22e7cae --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/LongEnum.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.TestCommon.Types +{ + public enum LongEnum : long + { + FirstLong, + SecondLong, + ThirdLong, + FourthLong + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeAssert.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeAssert.cs new file mode 100644 index 0000000000..1e414e57e1 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeAssert.cs @@ -0,0 +1,57 @@ +// 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.Net.Http.Headers; +using Xunit; +using Microsoft.AspNet.Mvc; + +namespace Microsoft.TestCommon +{ + public class MediaTypeAssert + { + private static readonly MediaTypeAssert singleton = new MediaTypeAssert(); + + public static MediaTypeAssert Singleton { get { return singleton; } } + + public void AreEqual(MediaTypeHeaderValue expected, MediaTypeHeaderValue actual, string errorMessage) + { + if (expected != null || actual != null) + { + Assert.NotNull(expected); + Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expected, actual)); + } + } + + public void AreEqual(MediaTypeHeaderValue expected, string actual, string errorMessage) + { + if (expected != null || !String.IsNullOrEmpty(actual)) + { + MediaTypeHeaderValue actualMediaType = new MediaTypeHeaderValue(actual); + Assert.NotNull(expected); + Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expected, actualMediaType)); + } + } + + public void AreEqual(string expected, string actual, string errorMessage) + { + if (!String.IsNullOrEmpty(expected) || !String.IsNullOrEmpty(actual)) + { + Assert.NotNull(expected); + MediaTypeHeaderValue expectedMediaType = new MediaTypeHeaderValue(expected); + MediaTypeHeaderValue actualMediaType = new MediaTypeHeaderValue(actual); + Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expectedMediaType, actualMediaType)); + } + } + + public void AreEqual(string expected, MediaTypeHeaderValue actual, string errorMessage) + { + if (!String.IsNullOrEmpty(expected) || actual != null) + { + Assert.NotNull(expected); + MediaTypeHeaderValue expectedMediaType = new MediaTypeHeaderValue(expected); + Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expectedMediaType, actual)); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeConstants.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeConstants.cs new file mode 100644 index 0000000000..191406571b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeConstants.cs @@ -0,0 +1,102 @@ +// 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.Net.Http.Headers; + +namespace System.Net.Http.Formatting +{ + /// + /// Constants related to media types. + /// + internal static class MediaTypeConstants + { + private static readonly MediaTypeHeaderValue _defaultApplicationXmlMediaType = new MediaTypeHeaderValue("application/xml"); + private static readonly MediaTypeHeaderValue _defaultTextXmlMediaType = new MediaTypeHeaderValue("text/xml"); + private static readonly MediaTypeHeaderValue _defaultApplicationJsonMediaType = new MediaTypeHeaderValue("application/json"); + private static readonly MediaTypeHeaderValue _defaultTextJsonMediaType = new MediaTypeHeaderValue("text/json"); + private static readonly MediaTypeHeaderValue _defaultApplicationOctetStreamMediaType = new MediaTypeHeaderValue("application/octet-stream"); + private static readonly MediaTypeHeaderValue _defaultApplicationFormUrlEncodedMediaType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + private static readonly MediaTypeHeaderValue _defaultApplicationBsonMediaType = new MediaTypeHeaderValue("application/bson"); + + /// + /// Gets a instance representing application/octet-stream. + /// + /// + /// A new instance representing application/octet-stream. + /// + public static MediaTypeHeaderValue ApplicationOctetStreamMediaType + { + get { return _defaultApplicationOctetStreamMediaType; } + } + + /// + /// Gets a instance representing application/xml. + /// + /// + /// A new instance representing application/xml. + /// + public static MediaTypeHeaderValue ApplicationXmlMediaType + { + get { return _defaultApplicationXmlMediaType; } + } + + /// + /// Gets a instance representing application/json. + /// + /// + /// A new instance representing application/json. + /// + public static MediaTypeHeaderValue ApplicationJsonMediaType + { + get { return _defaultApplicationJsonMediaType; } + } + + /// + /// Gets a instance representing text/xml. + /// + /// + /// A new instance representing text/xml. + /// + public static MediaTypeHeaderValue TextXmlMediaType + { + get { return _defaultTextXmlMediaType; } + } + + /// + /// Gets a instance representing text/json. + /// + /// + /// A new instance representing text/json. + /// + public static MediaTypeHeaderValue TextJsonMediaType + { + get { return _defaultTextJsonMediaType; } + } + + /// + /// Gets a instance representing application/x-www-form-urlencoded. + /// + /// + /// A new instance representing application/x-www-form-urlencoded. + /// + public static MediaTypeHeaderValue ApplicationFormUrlEncodedMediaType + { + get { return _defaultApplicationFormUrlEncodedMediaType; } + } + + /// + /// Gets a instance representing application/bson. + /// + /// + /// A new instance representing application/bson. + /// + /// + /// Not yet a standard. In particular this media type is not currently listed at + /// http://www.iana.org/assignments/media-types/application. + /// + public static MediaTypeHeaderValue ApplicationBsonMediaType + { + get { return _defaultApplicationBsonMediaType; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeHeaderValueComparer.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeHeaderValueComparer.cs new file mode 100644 index 0000000000..89edc53905 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/MediaTypeHeaderValueComparer.cs @@ -0,0 +1,195 @@ +// 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.Net.Http.Headers; + +namespace Microsoft.TestCommon +{ + public class MediaTypeHeaderValueComparer : IComparer + { + private static readonly MediaTypeHeaderValueComparer mediaTypeComparer = new MediaTypeHeaderValueComparer(); + + public MediaTypeHeaderValueComparer() + { + } + + public static MediaTypeHeaderValueComparer Comparer + { + get + { + return mediaTypeComparer; + } + } + + public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2) + { + ParsedMediaTypeHeaderValue parsedMediaType1 = new ParsedMediaTypeHeaderValue(mediaType1); + ParsedMediaTypeHeaderValue parsedMediaType2 = new ParsedMediaTypeHeaderValue(mediaType2); + + int returnValue = CompareBasedOnQualityFactor(parsedMediaType1, parsedMediaType2); + + if (returnValue == 0) + { + if (!String.Equals(parsedMediaType1.Type, parsedMediaType2.Type, StringComparison.OrdinalIgnoreCase)) + { + if (parsedMediaType1.IsAllMediaRange) + { + return 1; + } + else if (parsedMediaType2.IsAllMediaRange) + { + return -1; + } + } + else if (!String.Equals(parsedMediaType1.SubType, parsedMediaType2.SubType, StringComparison.OrdinalIgnoreCase)) + { + if (parsedMediaType1.IsSubTypeMediaRange) + { + return 1; + } + else if (parsedMediaType2.IsSubTypeMediaRange) + { + return -1; + } + } + else + { + if (!parsedMediaType1.HasNonQualityFactorParameter) + { + if (parsedMediaType2.HasNonQualityFactorParameter) + { + return 1; + } + } + else if (!parsedMediaType2.HasNonQualityFactorParameter) + { + return -1; + } + } + } + + return returnValue; + } + + private static int CompareBasedOnQualityFactor(ParsedMediaTypeHeaderValue parsedMediaType1, ParsedMediaTypeHeaderValue parsedMediaType2) + { + double qualityDifference = parsedMediaType1.QualityFactor - parsedMediaType2.QualityFactor; + if (qualityDifference < 0) + { + return 1; + } + else if (qualityDifference > 0) + { + return -1; + } + + return 0; + } + + internal class ParsedMediaTypeHeaderValue + { + private const string MediaRangeAsterisk = "*"; + private const char MediaTypeSubTypeDelimiter = '/'; + private const string QualityFactorParameterName = "q"; + private const double DefaultQualityFactor = 1.0; + + private MediaTypeHeaderValue mediaType; + private string type; + private string subType; + private bool? hasNonQualityFactorParameter; + private double? qualityFactor; + + public ParsedMediaTypeHeaderValue(MediaTypeHeaderValue mediaType) + { + this.mediaType = mediaType; + string[] splitMediaType = mediaType.MediaType.Split(MediaTypeSubTypeDelimiter); + this.type = splitMediaType[0]; + this.subType = splitMediaType[1]; + } + + public string Type + { + get + { + return this.type; + } + } + + public string SubType + { + get + { + return this.subType; + } + } + + public bool IsAllMediaRange + { + get + { + return this.IsSubTypeMediaRange && String.Equals(MediaRangeAsterisk, this.Type, StringComparison.Ordinal); + } + } + + public bool IsSubTypeMediaRange + { + get + { + return String.Equals(MediaRangeAsterisk, this.SubType, StringComparison.Ordinal); + } + } + + public bool HasNonQualityFactorParameter + { + get + { + if (!this.hasNonQualityFactorParameter.HasValue) + { + this.hasNonQualityFactorParameter = false; + foreach (NameValueHeaderValue param in this.mediaType.Parameters) + { + if (!String.Equals(QualityFactorParameterName, param.Name, StringComparison.Ordinal)) + { + this.hasNonQualityFactorParameter = true; + } + } + } + + return this.hasNonQualityFactorParameter.Value; + } + } + + public string CharSet + { + get + { + return this.mediaType.CharSet; + } + } + + public double QualityFactor + { + get + { + if (!this.qualityFactor.HasValue) + { + MediaTypeWithQualityHeaderValue mediaTypeWithQuality = this.mediaType as MediaTypeWithQualityHeaderValue; + if (mediaTypeWithQuality != null) + { + this.qualityFactor = mediaTypeWithQuality.Quality; + } + + if (!this.qualityFactor.HasValue) + { + this.qualityFactor = DefaultQualityFactor; + } + } + + return this.qualityFactor.Value; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/RefTypeTestData.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/RefTypeTestData.cs new file mode 100644 index 0000000000..09866eca4c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/RefTypeTestData.cs @@ -0,0 +1,76 @@ +// 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; + +namespace Microsoft.TestCommon +{ + public class RefTypeTestData : TestData where T : class + { + private Func> testDataProvider; + private Func> derivedTypeTestDataProvider; + private Func> knownTypeTestDataProvider; + + public RefTypeTestData(Func> testDataProvider) + { + if (testDataProvider == null) + { + throw new ArgumentNullException("testDataProvider"); + } + + this.testDataProvider = testDataProvider; + this.RegisterTestDataVariation(TestDataVariations.WithNull, this.Type, GetNullTestData); + } + + public RefTypeTestData( + Func> testDataProvider, + Func> derivedTypeTestDataProvider, + Func> knownTypeTestDataProvider) + : this(testDataProvider) + { + this.derivedTypeTestDataProvider = derivedTypeTestDataProvider; + if (this.derivedTypeTestDataProvider != null) + { + this.RegisterTestDataVariation(TestDataVariations.AsDerivedType, this.Type, this.GetTestDataAsDerivedType); + } + + this.knownTypeTestDataProvider = knownTypeTestDataProvider; + if (this.knownTypeTestDataProvider != null) + { + this.RegisterTestDataVariation(TestDataVariations.AsKnownType, this.Type, this.GetTestDataAsDerivedKnownType); + } + } + + public T GetNullTestData() + { + return null; + } + + public IEnumerable GetTestDataAsDerivedType() + { + if (this.derivedTypeTestDataProvider != null) + { + return this.derivedTypeTestDataProvider(); + } + + return Enumerable.Empty(); + } + + public IEnumerable GetTestDataAsDerivedKnownType() + { + if (this.knownTypeTestDataProvider != null) + { + return this.knownTypeTestDataProvider(); + } + + return Enumerable.Empty(); + } + + protected override IEnumerable GetTypedTestData() + { + return this.testDataProvider(); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/SimpleEnum.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/SimpleEnum.cs new file mode 100644 index 0000000000..cb5ca4e452 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/SimpleEnum.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.TestCommon.Types +{ + public enum SimpleEnum + { + First, + Second, + Third, + Fourth + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestData.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestData.cs new file mode 100644 index 0000000000..a674f3fc53 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestData.cs @@ -0,0 +1,441 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.TestCommon.Types; + +namespace Microsoft.TestCommon +{ + /// + /// A base class for test data. A instance is associated with a given type, and the instance can + /// provide instances of the given type to use as data in tests. The same instance can also provide instances + /// of types related to the given type, such as a of the type. See the enum for all the + /// variations of test data that a instance can provide. + /// + public abstract class TestData + { + /// + /// Common for a . + /// + public static readonly ValueTypeTestData CharTestData = new ValueTypeTestData('a', Char.MinValue, Char.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData IntTestData = new ValueTypeTestData(-1, 0, 1, Int32.MinValue, Int32.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData UintTestData = new ValueTypeTestData(0, 1, UInt32.MinValue, UInt32.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData ShortTestData = new ValueTypeTestData(-1, 0, 1, Int16.MinValue, Int16.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData UshortTestData = new ValueTypeTestData(0, 1, UInt16.MinValue, UInt16.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData LongTestData = new ValueTypeTestData(-1, 0, 1, Int64.MinValue, Int64.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData UlongTestData = new ValueTypeTestData(0, 1, UInt64.MinValue, UInt64.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData ByteTestData = new ValueTypeTestData(0, 1, Byte.MinValue, Byte.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData SByteTestData = new ValueTypeTestData(-1, 0, 1, SByte.MinValue, SByte.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData BoolTestData = new ValueTypeTestData(true, false); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData DoubleTestData = new ValueTypeTestData( + -1.0, + 0.0, + 1.0, + double.MinValue, + double.MaxValue, + double.PositiveInfinity, + double.NegativeInfinity); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData FloatTestData = new ValueTypeTestData( + -1.0f, + 0.0f, + 1.0f, + float.MinValue, + float.MaxValue, + float.PositiveInfinity, + float.NegativeInfinity); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData DecimalTestData = new ValueTypeTestData( + -1M, + 0M, + 1M, + decimal.MinValue, + decimal.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData DateTimeTestData = new ValueTypeTestData( + DateTime.Now, + DateTime.UtcNow, + DateTime.MaxValue, + DateTime.MinValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData TimeSpanTestData = new ValueTypeTestData( + TimeSpan.MinValue, + TimeSpan.MaxValue); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData GuidTestData = new ValueTypeTestData( + Guid.NewGuid(), + Guid.Empty); + + /// + /// Common for a . + /// + public static readonly ValueTypeTestData DateTimeOffsetTestData = new ValueTypeTestData( + DateTimeOffset.MaxValue, + DateTimeOffset.MinValue, + new DateTimeOffset(DateTime.Now)); + + /// + /// Common for an enum. + /// + public static readonly ValueTypeTestData SimpleEnumTestData = new ValueTypeTestData( + SimpleEnum.First, + SimpleEnum.Second, + SimpleEnum.Third); + + /// + /// Common for an enum implemented with a . + /// + public static readonly ValueTypeTestData LongEnumTestData = new ValueTypeTestData( + LongEnum.FirstLong, + LongEnum.SecondLong, + LongEnum.ThirdLong); + + /// + /// Common for an enum decorated with a . + /// + public static readonly ValueTypeTestData FlagsEnumTestData = new ValueTypeTestData( + FlagsEnum.One, + FlagsEnum.Two, + FlagsEnum.Four); + + /// + /// Expected permutations of non supported file paths. + /// + public static readonly TestData NotSupportedFilePaths = new RefTypeTestData(() => new List() { + "cc:\\a\\b", + }); + + /// + /// Expected permutations of invalid file paths. + /// + public static readonly TestData InvalidNonNullFilePaths = new RefTypeTestData(() => new List() { + String.Empty, + "", + " ", + " ", + "\t\t \n ", + "c:\\ab", + "c:\\a\"b", + "c:\\a\tb", + "c:\\a|b", + "c:\\a\bb", + "c:\\a\0b", + }); + + /// + /// All expected permutations of an empty string. + /// + public static readonly TestData NonNullEmptyStrings = new RefTypeTestData(() => new List() { String.Empty, "", " ", "\t\r\n" }); + + /// + /// All expected permutations of an empty string. + /// + public static readonly TestData EmptyStrings = new RefTypeTestData(() => new List() { null, String.Empty, "", " ", "\t\r\n" }); + + /// + /// Common for a . + /// + public static readonly RefTypeTestData StringTestData = new RefTypeTestData(() => new List() { + "", + " ", // one space + " ", // multiple spaces + " data ", // leading and trailing whitespace + "\t\t \n ", + "Some String!"}); + + /// + /// A read-only collection of value type test data. + /// + public static readonly ReadOnlyCollection ValueTypeTestDataCollection = new ReadOnlyCollection(new TestData[] { + CharTestData, + IntTestData, + UintTestData, + ShortTestData, + UshortTestData, + LongTestData, + UlongTestData, + ByteTestData, + SByteTestData, + BoolTestData, + DoubleTestData, + FloatTestData, + DecimalTestData, + TimeSpanTestData, + GuidTestData, + DateTimeOffsetTestData, + SimpleEnumTestData, + LongEnumTestData, + FlagsEnumTestData}); + + /// + /// A read-only collection of representative values and reference type test data. + /// Uses where exhaustive coverage is not required. + /// + public static readonly ReadOnlyCollection RepresentativeValueAndRefTypeTestDataCollection = new ReadOnlyCollection(new TestData[] { + IntTestData, + BoolTestData, + SimpleEnumTestData, + StringTestData, + }); + + private Dictionary registeredTestDataVariations; + + + /// + /// Initializes a new instance of the class. + /// + /// The type associated with the instance. + protected TestData(Type type) + { + this.Type = type; + this.registeredTestDataVariations = new Dictionary(); + } + + /// + /// Gets the type associated with the instance. + /// + public Type Type { get; private set; } + + + /// + /// Gets the supported test data variations. + /// + /// + public IEnumerable GetSupportedTestDataVariations() + { + return this.registeredTestDataVariations.Keys; + } + + /// + /// Gets the related type for the given test data variation or returns null if the instance + /// doesn't support the given variation. + /// + /// The test data variation with which to create the related . + /// The related for the as given by the test data variation. + /// + /// For example, if the given was created for test data and the varation parameter + /// was then the returned type would be . + /// + public Type GetAsTypeOrNull(TestDataVariations variation) + { + TestDataVariationProvider testDataVariation = null; + if (this.registeredTestDataVariations.TryGetValue(variation, out testDataVariation)) + { + return testDataVariation.Type; + } + + return null; + } + + /// + /// Gets test data for the given test data variation or returns null if the instance + /// doesn't support the given variation. + /// + /// The test data variation with which to create the related test data. + /// Test data of the type specified by the method. + public object GetAsTestDataOrNull(TestDataVariations variation) + { + TestDataVariationProvider testDataVariation = null; + if (this.registeredTestDataVariations.TryGetValue(variation, out testDataVariation)) + { + return testDataVariation.TestDataProvider(); + } + + return null; + } + + + /// + /// Allows derived classes to register a that will + /// provide test data for a given variation. + /// + /// The variation with which to register the r. + /// The type of the test data created by the + /// A that will provide test data. + protected void RegisterTestDataVariation(TestDataVariations variation, Type type, Func testDataProvider) + { + this.registeredTestDataVariations.Add(variation, new TestDataVariationProvider(type, testDataProvider)); + } + + private class TestDataVariationProvider + { + public TestDataVariationProvider(Type type, Func testDataProvider) + { + this.Type = type; + this.TestDataProvider = testDataProvider; + } + + + public Func TestDataProvider { get; private set; } + + public Type Type { get; private set; } + } + } + + + /// + /// A generic base class for test data. + /// + /// The type associated with the test data. + public abstract class TestData : TestData, IEnumerable + { + private static readonly Type OpenIEnumerableType = typeof(IEnumerable<>); + private static readonly Type OpenListType = typeof(List<>); + private static readonly Type OpenIQueryableType = typeof(IQueryable<>); + private static readonly Type OpenDictionaryType = typeof(Dictionary<,>); + private static readonly Type OpenTestDataHolderType = typeof(TestDataHolder<>); + private int dictionaryKey; + + /// + /// Initializes a new instance of the class. + /// + protected TestData() + : base(typeof(T)) + { + Type[] typeParams = new Type[] { this.Type }; + Type[] dictionaryTypeParams = new Type[] { typeof(string), this.Type }; + + Type arrayType = this.Type.MakeArrayType(); + Type listType = OpenListType.MakeGenericType(typeParams); + Type iEnumerableType = OpenIEnumerableType.MakeGenericType(typeParams); + Type iQueryableType = OpenIQueryableType.MakeGenericType(typeParams); + Type dictionaryType = OpenDictionaryType.MakeGenericType(dictionaryTypeParams); + Type testDataHolderType = OpenTestDataHolderType.MakeGenericType(typeParams); + + this.RegisterTestDataVariation(TestDataVariations.AsInstance, this.Type, () => GetTypedTestData()); + this.RegisterTestDataVariation(TestDataVariations.AsArray, arrayType, GetTestDataAsArray); + this.RegisterTestDataVariation(TestDataVariations.AsIEnumerable, iEnumerableType, GetTestDataAsIEnumerable); + this.RegisterTestDataVariation(TestDataVariations.AsIQueryable, iQueryableType, GetTestDataAsIQueryable); + this.RegisterTestDataVariation(TestDataVariations.AsList, listType, GetTestDataAsList); + this.RegisterTestDataVariation(TestDataVariations.AsDictionary, dictionaryType, GetTestDataAsDictionary); + this.RegisterTestDataVariation(TestDataVariations.AsClassMember, testDataHolderType, GetTestDataInHolder); + } + + public IEnumerator GetEnumerator() + { + return (IEnumerator)this.GetTypedTestData().ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)this.GetTypedTestData().ToList().GetEnumerator(); + } + + /// + /// Gets the test data as an array. + /// + /// An array of test data of the given type. + public T[] GetTestDataAsArray() + { + return this.GetTypedTestData().ToArray(); + } + + /// + /// Gets the test data as a . + /// + /// A of test data of the given type. + public List GetTestDataAsList() + { + return this.GetTypedTestData().ToList(); + } + + /// + /// Gets the test data as an . + /// + /// An of test data of the given type. + public IEnumerable GetTestDataAsIEnumerable() + { + return this.GetTypedTestData().AsEnumerable(); + } + + /// + /// Gets the test data as an . + /// + /// An of test data of the given type. + public IQueryable GetTestDataAsIQueryable() + { + //return this.GetTypedTestData().AsQueryable(); + return null; + } + + public Dictionary GetTestDataAsDictionary() + { + // Some TestData collections contain duplicates e.g. UintTestData contains both 0 and UInt32.MinValue. + // Therefore use dictionaryKey, not _unused.ToString(). Reset key to keep dictionaries consistent if used + // multiple times. + dictionaryKey = 0; + return this.GetTypedTestData().ToDictionary(_unused => (dictionaryKey++).ToString()); + } + + public IEnumerable> GetTestDataInHolder() + { + return this.GetTypedTestData().Select(value => new TestDataHolder { V1 = value, }); + } + + /// + /// Must be implemented by derived types to return test data of the given type. + /// + /// Test data of the given type. + protected abstract IEnumerable GetTypedTestData(); + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataHolder.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataHolder.cs new file mode 100644 index 0000000000..0923dc61f9 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataHolder.cs @@ -0,0 +1,51 @@ +// 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.Globalization; +using System.Reflection; + +namespace Microsoft.TestCommon +{ + /// + /// Equatable class wrapping a single instance of type . Equatable to ease test assertions. + /// + /// The to wrap. + public class TestDataHolder : IEquatable> + { + public T V1 { get; set; } + + bool IEquatable>.Equals(TestDataHolder other) + { + if (other == null) + { + return false; + } + + return Object.Equals(V1, other.V1); + } + + public override bool Equals(object obj) + { + TestDataHolder that = obj as TestDataHolder; + return ((IEquatable>)this).Equals(that); + } + + public override int GetHashCode() + { + if (typeof(ValueType).GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()) || V1 != null) + { + return V1.GetHashCode(); + } + else + { + return 0; + } + } + + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "{{ V1: '{0}' }}", V1); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataSetAttribute.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataSetAttribute.cs new file mode 100644 index 0000000000..dee704eea4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataSetAttribute.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit.Sdk; + +namespace Microsoft.TestCommon +{ + public class TestDataSetAttribute : DataAttribute + { + public Type DeclaringType { get; set; } + + public string PropertyName { get; set; } + + public TestDataVariations TestDataVariations { get; set; } + + private IEnumerable> ExtraDataSets { get; set; } + + public TestDataSetAttribute(Type declaringType, string propertyName, TestDataVariations testDataVariations = TestCommon.TestDataVariations.All) + { + DeclaringType = declaringType; + PropertyName = propertyName; + TestDataVariations = testDataVariations; + ExtraDataSets = new List>(); + } + + public TestDataSetAttribute(Type declaringType, string propertyName, + Type declaringType1, string propertyName1, + TestDataVariations testDataVariations = TestCommon.TestDataVariations.All) + : this(declaringType, propertyName, testDataVariations) + { + ExtraDataSets = new List> { Tuple.Create(declaringType1, propertyName1) }; + } + + public TestDataSetAttribute(Type declaringType, string propertyName, + Type declaringType1, string propertyName1, + Type declaringType2, string propertyName2, + TestDataVariations testDataVariations = TestCommon.TestDataVariations.All) + : this(declaringType, propertyName, testDataVariations) + { + ExtraDataSets = new List> { Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2) }; + } + + public TestDataSetAttribute(Type declaringType, string propertyName, + Type declaringType1, string propertyName1, + Type declaringType2, string propertyName2, + Type declaringType3, string propertyName3, + TestDataVariations testDataVariations = TestCommon.TestDataVariations.All) + : this(declaringType, propertyName, testDataVariations) + { + ExtraDataSets = new List> { Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2), Tuple.Create(declaringType3, propertyName3) }; + } + + public TestDataSetAttribute(Type declaringType, string propertyName, + Type declaringType1, string propertyName1, + Type declaringType2, string propertyName2, + Type declaringType3, string propertyName3, + Type declaringType4, string propertyName4, + TestDataVariations testDataVariations = TestCommon.TestDataVariations.All) + : this(declaringType, propertyName, testDataVariations) + { + ExtraDataSets = new List> + { + Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2), + Tuple.Create(declaringType3, propertyName3), Tuple.Create(declaringType4, propertyName4) + }; + } + + public override IEnumerable GetData(MethodInfo testMethod) + { + IEnumerable baseDataSet = GetBaseDataSet(DeclaringType, PropertyName, TestDataVariations); + IEnumerable> extraDataSets = GetExtraDataSets(); + + IEnumerable> finalDataSets = (new[] { baseDataSet }).Concat(extraDataSets); + + var datasets = CrossProduct(finalDataSets); + + return datasets; + } + + private static IEnumerable CrossProduct(IEnumerable> datasets) + { + if (datasets.Count() == 1) + { + foreach (var dataset in datasets.First()) + { + yield return dataset; + } + } + else + { + IEnumerable datasetLeft = datasets.First(); + IEnumerable datasetRight = CrossProduct(datasets.Skip(1)); + + foreach (var dataLeft in datasetLeft) + { + foreach (var dataRight in datasetRight) + { + yield return dataLeft.Concat(dataRight).ToArray(); + } + } + } + } + + // The base data set(first one) can either be a TestDataSet or a TestDataSetCollection + private static IEnumerable GetBaseDataSet(Type declaringType, string propertyName, TestDataVariations variations) + { + return TryGetDataSetFromTestDataCollection(declaringType, propertyName, variations) ?? GetDataSet(declaringType, propertyName); + } + + private IEnumerable> GetExtraDataSets() + { + foreach (var tuple in ExtraDataSets) + { + yield return GetDataSet(tuple.Item1, tuple.Item2); + } + } + + private static object GetTestDataPropertyValue(Type declaringType, string propertyName) + { + PropertyInfo property = declaringType.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public); + + if (property == null) + { + throw new ArgumentException(String.Format("Could not find public static property {0} on {1}", propertyName, declaringType.FullName)); + } + else + { + return property.GetValue(null, null); + } + } + + private static IEnumerable GetDataSet(Type declaringType, string propertyName) + { + object propertyValue = GetTestDataPropertyValue(declaringType, propertyName); + + // box the dataset items if the property is not a RefTypeTestData + IEnumerable value = (propertyValue as IEnumerable) ?? (propertyValue as IEnumerable).Cast(); + if (value == null) + { + throw new InvalidOperationException(String.Format("{0}.{1} is either null or does not implement IEnumerable", declaringType.FullName, propertyName)); + } + + IEnumerable dataset = value as IEnumerable; + if (dataset != null) + { + return dataset; + } + else + { + return value.Select((data) => new object[] { data }); + } + } + + private static IEnumerable TryGetDataSetFromTestDataCollection(Type declaringType, string propertyName, TestDataVariations variations) + { + object propertyValue = GetTestDataPropertyValue(declaringType, propertyName); + + IEnumerable testDataCollection = propertyValue as IEnumerable; + + return testDataCollection == null ? null : GetDataSetFromTestDataCollection(testDataCollection, variations); + } + + private static IEnumerable GetDataSetFromTestDataCollection(IEnumerable testDataCollection, TestDataVariations variations) + { + foreach (TestData testdataInstance in testDataCollection) + { + foreach (TestDataVariations variation in testdataInstance.GetSupportedTestDataVariations()) + { + if ((variation & variations) == variation) + { + Type variationType = testdataInstance.GetAsTypeOrNull(variation); + object testData = testdataInstance.GetAsTestDataOrNull(variation); + if (AsSingleInstances(variation)) + { + foreach (object obj in (IEnumerable)testData) + { + yield return new object[] { variationType, obj }; + } + } + else + { + yield return new object[] { variationType, testData }; + } + } + } + } + } + + private static bool AsSingleInstances(TestDataVariations variation) + { + return variation == TestDataVariations.AsInstance || + variation == TestDataVariations.AsNullable || + variation == TestDataVariations.AsDerivedType || + variation == TestDataVariations.AsKnownType || + variation == TestDataVariations.AsDataMember || + variation == TestDataVariations.AsClassMember || + variation == TestDataVariations.AsXmlElementProperty; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataVariations.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataVariations.cs new file mode 100644 index 0000000000..39dec03515 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TestDataVariations.cs @@ -0,0 +1,116 @@ +// 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; + +namespace Microsoft.TestCommon +{ + /// + /// An flags enum that can be used to indicate different variations of a given + /// instance. + /// + [Flags] + public enum TestDataVariations + { + /// + /// An individual instance of a given type. + /// + AsInstance = 0x1, + + /// + /// An individual instance of a type that derives from a given type. + /// + AsDerivedType = 0x2, + + /// + /// An individual instance of a given type that has a property value + /// that is a known type of the declared property type. + /// + AsKnownType = 0x4, + + /// + /// A instance of a given type. Only applies to + /// instances of . + /// + AsNullable = 0x8, + + /// + /// An instance of a of a given type. + /// + AsList = 0x10, + + /// + /// An instance of a array of the type. + /// + AsArray = 0x20, + + /// + /// An instance of an of a given type. + /// + AsIEnumerable = 0x40, + + /// + /// An instance of an of a given type. + /// + AsIQueryable = 0x80, + + /// + /// An instance of a DataContract type in which a given type is a member. + /// + AsDataMember = 0x100, + + /// + /// An instance of a type in which a given type is decorated with a + /// . + /// + AsXmlElementProperty = 0x200, + + /// + /// An instance of a of a given + /// type. + /// + AsDictionary = 0x400, + + /// + /// Add a null instance of the given type to the data set. This variation is + /// not included in or other variation masks. + /// + WithNull = 0x800, + + /// + /// Individual instances of containing the given . This + /// variation is not included in or other variation masks. + /// + AsClassMember = 0x1000, + + /// + /// All of the flags for single instance variations of a given type. + /// + AllSingleInstances = AsInstance | AsDerivedType | AsKnownType | AsNullable, + + /// + /// All of the flags for collection variations of a given type. + /// + AllCollections = AsList | AsArray | AsIEnumerable | AsIQueryable | AsDictionary, + + /// + /// All of the flags for variations in which a given type is a property on another type. + /// + AllProperties = AsDataMember | AsXmlElementProperty, + + /// + /// All of the flags for interface collection variations of a given type. + /// + AllInterfaces = AsIEnumerable | AsIQueryable, + + /// + /// All of the flags except for the interface collection variations of a given type. + /// + AllNonInterfaces = All & ~AllInterfaces, + + /// + /// All of the flags for all of the variations of a given type. + /// + All = AllSingleInstances | AllCollections | AllProperties + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TypeAssert.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TypeAssert.cs new file mode 100644 index 0000000000..e1313991f6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/TypeAssert.cs @@ -0,0 +1,165 @@ +// 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.Reflection; +using Xunit; + +namespace Microsoft.TestCommon +{ + /// + /// MSTest utility for testing that a given type has the expected properties such as being public, sealed, etc. + /// + public class TypeAssert + { + /// + /// Specifies a set of type properties to test for using the method. + /// This enumeration has a attribute that allows a bitwise combination of its member values. + /// + [Flags] + public enum TypeProperties + { + /// + /// Indicates that the type must be abstract. + /// + IsAbstract = 0x1, + + /// + /// Indicates that the type must be a class. + /// + IsClass = 0x2, + + /// + /// Indicates that the type must be a COM object. + /// + IsComObject = 0x4, + + /// + /// Indicates that the type must be disposable. + /// + IsDisposable = 0x8, + + /// + /// Indicates that the type must be an enum. + /// + IsEnum = 0x10, + + /// + /// Indicates that the type must be a generic type. + /// + IsGenericType = 0x20, + + /// + /// Indicates that the type must be a generic type definition. + /// + IsGenericTypeDefinition = 0x40, + + /// + /// Indicates that the type must be an interface. + /// + IsInterface = 0x80, + + /// + /// Indicates that the type must be nested and declared private. + /// + IsNestedPrivate = 0x100, + + /// + /// Indicates that the type must be nested and declared public. + /// + IsNestedPublic = 0x200, + + /// + /// Indicates that the type must be public. + /// + IsPublic = 0x400, + + /// + /// Indicates that the type must be sealed. + /// + IsSealed = 0x800, + + /// + /// Indicates that the type must be visible outside the assembly. + /// + IsVisible = 0x1000, + + /// + /// Indicates that the type must be static. + /// + IsStatic = TypeAssert.TypeProperties.IsAbstract | TypeAssert.TypeProperties.IsSealed, + + /// + /// Indicates that the type must be a public, visible class. + /// + IsPublicVisibleClass = TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsPublic | TypeAssert.TypeProperties.IsVisible + } + + private static void CheckProperty(Type type, bool expected, bool actual, string property) + { + Assert.NotNull(type); + Assert.True(expected == actual, String.Format("Type '{0}' should{1} be {2}.", type.FullName, expected ? "" : " NOT", property)); + } + + /// + /// Determines whether the specified type has a given set of properties such as being public, sealed, etc. + /// The method asserts if one or more of the properties are not satisfied. + /// + /// The type to test for properties. + /// The set of type properties to test for. + public void HasProperties(TypeProperties typeProperties) + { + HasProperties(typeof(T), typeProperties); + } + + /// + /// Determines whether the specified type has a given set of properties such as being public, sealed, etc. + /// The method asserts if one or more of the properties are not satisfied. + /// + /// The type to test for properties. + /// Verify that the type to test is assignable from this type. + /// The set of type properties to test for. + public void HasProperties(TypeProperties typeProperties) + { + HasProperties(typeof(T), typeProperties, typeof(TIsAssignableFrom)); + } + + /// + /// Determines whether the specified type has a given set of properties such as being public, sealed, etc. + /// The method asserts if one or more of the properties are not satisfied. + /// + /// The type to test for properties. + /// The set of type properties to test for. + public void HasProperties(Type type, TypeProperties typeProperties) + { + HasProperties(type, typeProperties, null); + } + + /// + /// Determines whether the specified type has a given set of properties such as being public, sealed, etc. + /// The method asserts if one or more of the properties are not satisfied. + /// + /// The type to test for properties. + /// The set of type properties to test for. + /// Verify that the type to test is assignable from this type. + public void HasProperties(Type type, TypeProperties typeProperties, Type isAssignableFrom) + { + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsAbstract) > 0, type.GetTypeInfo().IsAbstract, "abstract"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsClass) > 0, type.GetTypeInfo().IsClass, "a class"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsDisposable) > 0, typeof(IDisposable).IsAssignableFrom(type), "disposable"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsEnum) > 0, type.GetTypeInfo().IsEnum, "an enum"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsGenericType) > 0, type.GetTypeInfo().IsGenericType, "a generic type"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsGenericTypeDefinition) > 0, type.GetTypeInfo().IsGenericTypeDefinition, "a generic type definition"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsInterface) > 0, type.GetTypeInfo().IsInterface, "an interface"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsNestedPrivate) > 0, type.GetTypeInfo().IsNestedPrivate, "nested private"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsNestedPublic) > 0, type.GetTypeInfo().IsNestedPublic, "nested public"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsPublic) > 0, type.GetTypeInfo().IsPublic, "public"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsSealed) > 0, type.GetTypeInfo().IsSealed, "sealed"); + TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsVisible) > 0, type.GetTypeInfo().IsVisible, "visible"); + if (isAssignableFrom != null) + { + TypeAssert.CheckProperty(type, true, isAssignableFrom.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()), String.Format("assignable from {0}", isAssignableFrom.FullName)); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/ValueTypeTestData.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/ValueTypeTestData.cs new file mode 100644 index 0000000000..8923b02afb --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/TestUtils/ValueTypeTestData.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; + +namespace Microsoft.TestCommon +{ + public class ValueTypeTestData : TestData where T : struct + { + private static readonly Type OpenNullableType = typeof(Nullable<>); + private T[] testData; + + public ValueTypeTestData(params T[] testData) + : base() + { + this.testData = testData; + + Type[] typeParams = new Type[] { this.Type }; + this.RegisterTestDataVariation(TestDataVariations.WithNull, OpenNullableType.MakeGenericType(typeParams), GetNullTestData); + this.RegisterTestDataVariation(TestDataVariations.AsNullable, OpenNullableType.MakeGenericType(typeParams), GetTestDataAsNullable); + } + + public object GetNullTestData() + { + return null; + } + + public IEnumerable> GetTestDataAsNullable() + { + return this.GetTypedTestData().Select(d => new Nullable(d)); + } + + protected override IEnumerable GetTypedTestData() + { + return this.testData; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json index e92790191d..36782494cf 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json @@ -1,6 +1,6 @@ { "compilationOptions": { - "warningsAsErrors": "true" + "warningsAsErrors": false }, "dependencies": { "Microsoft.AspNet.Mvc": "6.0.0-*", @@ -13,6 +13,15 @@ "test": "Xunit.KRunner" }, "frameworks": { - "aspnet50": { } + "aspnet50": { + "frameworkAssemblies": { + "System.Net.Http": "4.0.0.0" + } + }, + "aspnetcore50": { + "dependencies": { + "System.Net.Http": "4.0.0-beta-*" + } + } } } diff --git a/test/WebSites/WebApiCompatShimWebSite/project.json b/test/WebSites/WebApiCompatShimWebSite/project.json index 2f16c0d99c..38646cbc06 100644 --- a/test/WebSites/WebApiCompatShimWebSite/project.json +++ b/test/WebSites/WebApiCompatShimWebSite/project.json @@ -6,6 +6,7 @@ "Microsoft.AspNet.Server.IIS": "1.0.0-*" }, "frameworks": { - "aspnet50": { } + "aspnet50": { }, + "aspnetcore50": { } } }