diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs index 271329209a..ebb3995472 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Authentication { Items[key] = value; } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } @@ -169,9 +169,9 @@ namespace Microsoft.AspNetCore.Authentication { if (value.HasValue) { - Items[key] = value.Value.ToString(); + Items[key] = value.GetValueOrDefault().ToString(); } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } @@ -201,9 +201,9 @@ namespace Microsoft.AspNetCore.Authentication { if (value.HasValue) { - Items[key] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + Items[key] = value.GetValueOrDefault().ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs index 555da9e098..e188e98823 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Authentication { /// - /// Name/Value representing an token. + /// Name/Value representing a token. /// public class AuthenticationToken { @@ -19,4 +19,4 @@ namespace Microsoft.AspNetCore.Authentication /// public string Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs index 639c9b558e..84db381ce4 100644 --- a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs +++ b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Xunit; @@ -73,6 +74,10 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test props.SetString("foo", null); Assert.Null(props.GetString("foo")); Assert.Equal(1, props.Items.Count); + + props.SetString("doesntexist", null); + Assert.False(props.Items.ContainsKey("doesntexist")); + Assert.Equal(1, props.Items.Count); } [Fact] @@ -203,5 +208,94 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test props.Items.Clear(); Assert.Null(props.AllowRefresh); } + + [Fact] + public void SetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); + + props.SetDateTimeOffset("foo", new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc))); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items["foo"]); + + props.SetDateTimeOffset("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + + props.SetDateTimeOffset("doesnotexist", null); + Assert.False(props.Items.ContainsKey("doesnotexist")); + } + + [Fact] + public void GetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); + var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + + props.Items["foo"] = dateTimeOffset.ToString("r", CultureInfo.InvariantCulture); + Assert.Equal(dateTimeOffset, props.GetDateTimeOffset("foo")); + + props.Items.Remove("foo"); + Assert.Null(props.GetDateTimeOffset("foo")); + + props.Items["foo"] = "BAR"; + Assert.Null(props.GetDateTimeOffset("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } + + [Fact] + public void SetBool() + { + var props = new MyAuthenticationProperties(); + + props.SetBool("foo", true); + Assert.Equal(true.ToString(), props.Items["foo"]); + + props.SetBool("foo", false); + Assert.Equal(false.ToString(), props.Items["foo"]); + + props.SetBool("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + } + + [Fact] + public void GetBool() + { + var props = new MyAuthenticationProperties(); + + props.Items["foo"] = true.ToString(); + Assert.True(props.GetBool("foo")); + + props.Items["foo"] = false.ToString(); + Assert.False(props.GetBool("foo")); + + props.Items["foo"] = null; + Assert.Null(props.GetBool("foo")); + + props.Items["foo"] = "BAR"; + Assert.Null(props.GetBool("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } + + public class MyAuthenticationProperties : AuthenticationProperties + { + public new DateTimeOffset? GetDateTimeOffset(string key) + { + return base.GetDateTimeOffset(key); + } + + public new void SetDateTimeOffset(string key, DateTimeOffset? value) + { + base.SetDateTimeOffset(key, value); + } + + public new void SetBool(string key, bool? value) + { + base.SetBool(key, value); + } + + public new bool? GetBool(string key) + { + return base.GetBool(key); + } + } } } diff --git a/src/Http/Headers/src/CacheControlHeaderValue.cs b/src/Http/Headers/src/CacheControlHeaderValue.cs index 81e18faf47..655d9447ce 100644 --- a/src/Http/Headers/src/CacheControlHeaderValue.cs +++ b/src/Http/Headers/src/CacheControlHeaderValue.cs @@ -197,14 +197,14 @@ namespace Microsoft.Net.Http.Headers { AppendValueWithSeparatorIfRequired(sb, MaxAgeString); sb.Append('='); - sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_sharedMaxAge.HasValue) { AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString); sb.Append('='); - sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_maxStale) @@ -213,7 +213,7 @@ namespace Microsoft.Net.Http.Headers if (_maxStaleLimit.HasValue) { sb.Append('='); - sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } } @@ -221,7 +221,7 @@ namespace Microsoft.Net.Http.Headers { AppendValueWithSeparatorIfRequired(sb, MinFreshString); sb.Append('='); - sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_private) @@ -291,10 +291,10 @@ namespace Microsoft.Net.Http.Headers // XOR the hashcode of timespan values with different numbers to make sure two instances with the same // timespan set on different fields result in different hashcodes. - result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^ - (_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^ - (_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^ - (_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0); + result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^ + (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^ + (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^ + (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0); if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) { diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index 392a441733..5084f8ea06 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -137,11 +137,11 @@ namespace Microsoft.Net.Http.Headers } else if (sizeParameter != null) { - sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture); + sizeParameter.Value = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); } else { - string sizeString = value.Value.ToString(CultureInfo.InvariantCulture); + string sizeString = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); _parameters.Add(new NameValueHeaderValue(SizeString, sizeString)); } } @@ -324,7 +324,7 @@ namespace Microsoft.Net.Http.Headers else { // Must always be quoted - var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true); + var dateString = HeaderUtilities.FormatDate(date.GetValueOrDefault(), quoted: true); if (dateParameter != null) { dateParameter.Value = dateString; diff --git a/src/Http/Headers/src/ContentRangeHeaderValue.cs b/src/Http/Headers/src/ContentRangeHeaderValue.cs index 99583cdf47..1149c33865 100644 --- a/src/Http/Headers/src/ContentRangeHeaderValue.cs +++ b/src/Http/Headers/src/ContentRangeHeaderValue.cs @@ -151,9 +151,9 @@ namespace Microsoft.Net.Http.Headers if (HasRange) { - sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); sb.Append('-'); - sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); } else { @@ -163,7 +163,7 @@ namespace Microsoft.Net.Http.Headers sb.Append('/'); if (HasLength) { - sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_length.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); } else { diff --git a/src/Http/Headers/src/RangeConditionHeaderValue.cs b/src/Http/Headers/src/RangeConditionHeaderValue.cs index f1ebee276c..b1d6513e2e 100644 --- a/src/Http/Headers/src/RangeConditionHeaderValue.cs +++ b/src/Http/Headers/src/RangeConditionHeaderValue.cs @@ -54,7 +54,7 @@ namespace Microsoft.Net.Http.Headers { if (_entityTag == null) { - return HeaderUtilities.FormatDate(_lastModified.Value); + return HeaderUtilities.FormatDate(_lastModified.GetValueOrDefault()); } return _entityTag.ToString(); } @@ -70,7 +70,7 @@ namespace Microsoft.Net.Http.Headers if (_entityTag == null) { - return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value); + return (other._lastModified != null) && (_lastModified.GetValueOrDefault() == other._lastModified.GetValueOrDefault()); } return _entityTag.Equals(other._entityTag); @@ -80,7 +80,7 @@ namespace Microsoft.Net.Http.Headers { if (_entityTag == null) { - return _lastModified.Value.GetHashCode(); + return _lastModified.GetValueOrDefault().GetHashCode(); } return _entityTag.GetHashCode(); diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs index 3b177f6e9a..010f2da1e0 100644 --- a/src/Http/Headers/src/RangeItemHeaderValue.cs +++ b/src/Http/Headers/src/RangeItemHeaderValue.cs @@ -20,15 +20,15 @@ namespace Microsoft.Net.Http.Headers { throw new ArgumentException("Invalid header range."); } - if (from.HasValue && (from.Value < 0)) + if (from.HasValue && (from.GetValueOrDefault() < 0)) { throw new ArgumentOutOfRangeException(nameof(from)); } - if (to.HasValue && (to.Value < 0)) + if (to.HasValue && (to.GetValueOrDefault() < 0)) { throw new ArgumentOutOfRangeException(nameof(to)); } - if (from.HasValue && to.HasValue && (from.Value > to.Value)) + if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault())) { throw new ArgumentOutOfRangeException(nameof(from)); } @@ -51,38 +51,37 @@ namespace Microsoft.Net.Http.Headers { if (!_from.HasValue) { - return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo); + return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); } else if (!_to.HasValue) { - return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-"; + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-"; } - return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" + - _to.Value.ToString(NumberFormatInfo.InvariantInfo); + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" + + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); } public override bool Equals(object obj) { - var other = obj as RangeItemHeaderValue; - - if (other == null) + if (obj is RangeItemHeaderValue other) { - return false; + return ((_from == other._from) && (_to == other._to)); } - return ((_from == other._from) && (_to == other._to)); + + return false; } public override int GetHashCode() { if (!_from.HasValue) { - return _to.GetHashCode(); + return _to.GetValueOrDefault().GetHashCode(); } else if (!_to.HasValue) { - return _from.GetHashCode(); + return _from.GetValueOrDefault().GetHashCode(); } - return _from.GetHashCode() ^ _to.GetHashCode(); + return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode(); } // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs index f3477648de..3070a6e1f9 100644 --- a/src/Http/Headers/src/SetCookieHeaderValue.cs +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -105,13 +105,13 @@ namespace Microsoft.Net.Http.Headers if (Expires.HasValue) { - expires = HeaderUtilities.FormatDate(Expires.Value); + expires = HeaderUtilities.FormatDate(Expires.GetValueOrDefault()); length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length; } if (MaxAge.HasValue) { - maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds); + maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds); length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length; } @@ -212,12 +212,12 @@ namespace Microsoft.Net.Http.Headers if (Expires.HasValue) { - AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); + AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.GetValueOrDefault())); } if (MaxAge.HasValue) { - AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds)); + AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds)); } if (Domain != null) @@ -452,9 +452,15 @@ namespace Microsoft.Net.Http.Headers result.HttpOnly = true; } // extension-av = - else - { - // TODO: skip it? Store it in a list? + else + { + // TODO: skiping it for now to avoid parsing failure? Store it in a list? + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + ReadToSemicolonOrEnd(input, ref offset); } } @@ -520,4 +526,4 @@ namespace Microsoft.Net.Http.Headers ^ HttpOnly.GetHashCode(); } } -} \ No newline at end of file +} diff --git a/src/Http/Headers/src/StringWithQualityHeaderValue.cs b/src/Http/Headers/src/StringWithQualityHeaderValue.cs index deba2d2697..153643b361 100644 --- a/src/Http/Headers/src/StringWithQualityHeaderValue.cs +++ b/src/Http/Headers/src/StringWithQualityHeaderValue.cs @@ -58,7 +58,7 @@ namespace Microsoft.Net.Http.Headers { if (_quality.HasValue) { - return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo); + return _value + "; q=" + _quality.GetValueOrDefault().ToString("0.0##", NumberFormatInfo.InvariantInfo); } return _value.ToString(); @@ -83,7 +83,7 @@ namespace Microsoft.Net.Http.Headers // Note that we don't consider double.Epsilon here. We really consider two values equal if they're // actually equal. This makes sure that we also get the same hashcode for two values considered equal // by Equals(). - return other._quality.HasValue && (_quality.Value == other._quality.Value); + return other._quality.HasValue && (_quality.GetValueOrDefault() == other._quality.Value); } // If we don't have a quality value, then 'other' must also have no quality assigned in order to be @@ -97,7 +97,7 @@ namespace Microsoft.Net.Http.Headers if (_quality.HasValue) { - result = result ^ _quality.Value.GetHashCode(); + result = result ^ _quality.GetValueOrDefault().GetHashCode(); } return result; diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs index 9a920f40d0..058f8d4bd9 100644 --- a/src/Http/Headers/test/SetCookieHeaderValueTest.cs +++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs @@ -365,6 +365,18 @@ namespace Microsoft.Net.Http.Headers Assert.Equal(cookies, results); } + [Fact] + public void SetCookieHeaderValue_TryParse_SkipExtensionValues() + { + string cookieHeaderValue = "cookiename=value; extensionname=value;"; + + SetCookieHeaderValue setCookieHeaderValue; + + SetCookieHeaderValue.TryParse(cookieHeaderValue, out setCookieHeaderValue); + + Assert.Equal("value", setCookieHeaderValue.Value); + } + [Theory] [MemberData(nameof(ListOfSetCookieHeaderDataSet))] public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs index ce89e5b054..5c0db2a46f 100644 --- a/src/Http/Http.Abstractions/src/CookieBuilder.cs +++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Http Domain = Domain, IsEssential = IsEssential, Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), - Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.Value) : default(DateTimeOffset?) + Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.GetValueOrDefault()) : default(DateTimeOffset?) }; } } diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs index a4337b7766..4c4d0d1af1 100644 --- a/src/Http/Http.Abstractions/src/HttpRequest.cs +++ b/src/Http/Http.Abstractions/src/HttpRequest.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Http { @@ -66,9 +67,9 @@ namespace Microsoft.AspNetCore.Http public abstract IQueryCollection Query { get; set; } /// - /// Gets or sets the RequestProtocol. + /// Gets or sets the request protocol (e.g. HTTP/1.1). /// - /// The RequestProtocol. + /// The request protocol. public abstract string Protocol { get; set; } /// @@ -117,5 +118,11 @@ namespace Microsoft.AspNetCore.Http /// /// public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Gets the collection of route values for this request. + /// + /// The collection of route values for this request. + public virtual RouteValueDictionary RouteValues { get; set; } } } diff --git a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs index 185fc40ac7..971447fa44 100644 --- a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs +++ b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs @@ -103,13 +103,13 @@ namespace Microsoft.AspNetCore.Http.Internal { throw new ArgumentNullException(nameof(key)); } - if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) + if (!values.HasValue || StringValues.IsNullOrEmpty(values.GetValueOrDefault())) { headers.Remove(key); } else { - headers[key] = values.Value; + headers[key] = values.GetValueOrDefault(); } } diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 821b40cb19..b95ea9c28c 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -17,6 +17,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs index 6af7d138be..02985d630b 100644 --- a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs +++ b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs @@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions internal static string FormatArgumentCannotBeNullOrEmpty() => GetString("ArgumentCannotBeNullOrEmpty"); + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string RouteValueDictionary_DuplicateKey + { + get => GetString("RouteValueDictionary_DuplicateKey"); + } + + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1); + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string RouteValueDictionary_DuplicatePropertyName + { + get => GetString("RouteValueDictionary_DuplicatePropertyName"); + } + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Http/Http.Abstractions/src/Resources.resx b/src/Http/Http.Abstractions/src/Resources.resx index dfdfeaf7d1..4ce2d8bd7a 100644 --- a/src/Http/Http.Abstractions/src/Resources.resx +++ b/src/Http/Http.Abstractions/src/Resources.resx @@ -156,4 +156,10 @@ Argument cannot be null or empty. + + An element with the key '{0}' already exists in the {1}. + + + The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Routing/Endpoint.cs b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs new file mode 100644 index 0000000000..16202b45f5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Respresents a logical endpoint in an application. + /// + public class Endpoint + { + /// + /// Creates a new instance of . + /// + /// The delegate used to process requests for the endpoint. + /// + /// The endpoint . May be null. + /// + /// + /// The informational display name of the endpoint. May be null. + /// + public Endpoint( + RequestDelegate requestDelegate, + EndpointMetadataCollection metadata, + string displayName) + { + // All are allowed to be null + RequestDelegate = requestDelegate; + Metadata = metadata ?? EndpointMetadataCollection.Empty; + DisplayName = displayName; + } + + /// + /// Gets the informational display name of this endpoint. + /// + public string DisplayName { get; } + + /// + /// Gets the collection of metadata associated with this endpoint. + /// + public EndpointMetadataCollection Metadata { get; } + + /// + /// Gets the delegate used to process requests for the endpoint. + /// + public RequestDelegate RequestDelegate { get; } + + public override string ToString() => DisplayName ?? base.ToString(); + } +} diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs new file mode 100644 index 0000000000..642af5f4d5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http.Endpoints +{ + /// + /// Extension methods to expose Endpoint on HttpContext. + /// + public static class EndpointHttpContextExtensions + { + /// + /// Extension method for getting the for the current request. + /// + /// The context. + /// The . + public static Endpoint GetEndpoint(this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Endpoint; + } + + /// + /// Extension method for setting the for the current request. + /// + /// The context. + /// The . + public static void SetEndpoint(this HttpContext context, Endpoint endpoint) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var feature = context.Features.Get(); + + if (endpoint != null) + { + if (feature == null) + { + feature = new EndpointFeature(); + context.Features.Set(feature); + } + + feature.Endpoint = endpoint; + } + else + { + if (feature == null) + { + // No endpoint to set and no feature on context. Do nothing + return; + } + + feature.Endpoint = null; + } + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } + } +} diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs new file mode 100644 index 0000000000..a792fff295 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs @@ -0,0 +1,201 @@ +// Copyright (c) .NET Foundation. 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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A collection of arbitrary metadata associated with an endpoint. + /// + /// + /// instances contain a list of metadata items + /// of arbitrary types. The metadata items are stored as an ordered collection with + /// items arranged in ascending order of precedence. + /// + public sealed class EndpointMetadataCollection : IReadOnlyList + { + /// + /// An empty . + /// + public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); + + private readonly object[] _items; + private readonly ConcurrentDictionary _cache; + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _items = items.ToArray(); + _cache = new ConcurrentDictionary(); + } + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(params object[] items) + : this((IEnumerable)items) + { + } + + /// + /// Gets the item at . + /// + /// The index of the item to retrieve. + /// The item at . + public object this[int index] => _items[index]; + + /// + /// Gets the count of metadata items. + /// + public int Count => _items.Length; + + /// + /// Gets the most significant metadata item of type . + /// + /// The type of metadata to retrieve. + /// + /// The most significant metadata of type or null. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + var length = result.Length; + return length > 0 ? (T)result[length - 1] : default; + } + + return GetMetadataSlow(); + } + + private T GetMetadataSlow() where T : class + { + var array = GetOrderedMetadataSlow(); + var length = array.Length; + return length > 0 ? array[length - 1] : default; + } + + /// + /// Gets the metadata items of type in ascending + /// order of precedence. + /// + /// The type of metadata. + /// A sequence of metadata items of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable GetOrderedMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + return (T[])result; + } + + return GetOrderedMetadataSlow(); + } + + private T[] GetOrderedMetadataSlow() where T : class + { + var items = new List(); + for (var i = 0; i < _items.Length; i++) + { + if (_items[i] is T item) + { + items.Add(item); + } + } + + var array = items.ToArray(); + _cache.TryAdd(typeof(T), array); + return array; + } + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of an . + /// + public struct Enumerator : IEnumerator + { + // Intentionally not readonly to prevent defensive struct copies + private object[] _items; + private int _index; + + internal Enumerator(EndpointMetadataCollection collection) + { + _items = collection._items; + _index = 0; + Current = null; + } + + /// + /// Gets the element at the current position of the enumerator + /// + public object Current { get; private set; } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (_index < _items.Length) + { + Current = _items[_index++]; + return true; + } + + Current = null; + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _index = 0; + Current = null; + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs b/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs new file mode 100644 index 0000000000..ff0762bb20 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for endpoint routing. Use + /// to access an instance associated with the current request. + /// + public interface IEndpointFeature + { + /// + /// Gets or sets the selected for the current + /// request. + /// + Endpoint Endpoint { get; set; } + } +} diff --git a/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs b/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs new file mode 100644 index 0000000000..d1c91577fe --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for routing values. Use + /// to access the values associated with the current request. + /// + public interface IRouteValuesFeature + { + /// + /// Gets or sets the associated with the currrent + /// request. + /// + RouteValueDictionary RouteValues { get; set; } + } +} diff --git a/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs new file mode 100644 index 0000000000..13c433b387 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs @@ -0,0 +1,768 @@ +// Copyright (c) .NET Foundation. 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// An type for route values. + /// + public class RouteValueDictionary : IDictionary, IReadOnlyDictionary + { + // 4 is a good default capacity here because that leaves enough space for area/controller/action/id + private const int DefaultCapacity = 4; + + internal KeyValuePair[] _arrayStorage; + internal PropertyStorage _propertyStorage; + private int _count; + + /// + /// Creates a new from the provided array. + /// The new instance will take ownership of the array, and may mutate it. + /// + /// The items array. + /// A new . + public static RouteValueDictionary FromArray(KeyValuePair[] items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + // We need to compress the array by removing non-contiguous items. We + // typically have a very small number of items to process. We don't need + // to preserve order. + var start = 0; + var end = items.Length - 1; + + // We walk forwards from the beginning of the array and fill in 'null' slots. + // We walk backwards from the end of the array end move items in non-null' slots + // into whatever start is pointing to. O(n) + while (start <= end) + { + if (items[start].Key != null) + { + start++; + } + else if (items[end].Key != null) + { + // Swap this item into start and advance + items[start] = items[end]; + items[end] = default; + start++; + end--; + } + else + { + // Both null, we need to hold on 'start' since we + // still need to fill it with something. + end--; + } + } + + return new RouteValueDictionary() + { + _arrayStorage = items, + _count = start, + }; + } + + /// + /// Creates an empty . + /// + public RouteValueDictionary() + { + _arrayStorage = Array.Empty>(); + } + + /// + /// Creates a initialized with the specified . + /// + /// An object to initialize the dictionary. The value can be of type + /// or + /// or an object with public properties as key-value pairs. + /// + /// + /// If the value is a dictionary or other of , + /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the + /// property names are keys, and property values are the values, and copied into the dictionary. + /// Only public instance non-index properties are considered. + /// + public RouteValueDictionary(object values) + { + if (values is RouteValueDictionary dictionary) + { + if (dictionary._propertyStorage != null) + { + // PropertyStorage is immutable so we can just copy it. + _propertyStorage = dictionary._propertyStorage; + _count = dictionary._count; + return; + } + + var count = dictionary._count; + if (count > 0) + { + var other = dictionary._arrayStorage; + var storage = new KeyValuePair[count]; + Array.Copy(other, 0, storage, 0, count); + _arrayStorage = storage; + _count = count; + } + else + { + _arrayStorage = Array.Empty>(); + } + + return; + } + + if (values is IEnumerable> keyValueEnumerable) + { + _arrayStorage = Array.Empty>(); + + foreach (var kvp in keyValueEnumerable) + { + Add(kvp.Key, kvp.Value); + } + + return; + } + + if (values is IEnumerable> stringValueEnumerable) + { + _arrayStorage = Array.Empty>(); + + foreach (var kvp in stringValueEnumerable) + { + Add(kvp.Key, kvp.Value); + } + + return; + } + + if (values != null) + { + var storage = new PropertyStorage(values); + _propertyStorage = storage; + _count = storage.Properties.Length; + } + else + { + _arrayStorage = Array.Empty>(); + } + } + + /// + public object this[string key] + { + get + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + object value; + TryGetValue(key, out value); + return value; + } + + set + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + // We're calling this here for the side-effect of converting from properties + // to array. We need to create the array even if we just set an existing value since + // property storage is immutable. + EnsureCapacity(_count); + + var index = FindIndex(key); + if (index < 0) + { + EnsureCapacity(_count + 1); + _arrayStorage[_count++] = new KeyValuePair(key, value); + } + else + { + _arrayStorage[index] = new KeyValuePair(key, value); + } + } + } + + /// + /// Gets the comparer for this dictionary. + /// + /// + /// This will always be a reference to + /// + public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; + + /// + public int Count => _count; + + /// + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys + { + get + { + EnsureCapacity(_count); + + var array = _arrayStorage; + var keys = new string[_count]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = array[i].Key; + } + + return keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + public ICollection Values + { + get + { + EnsureCapacity(_count); + + var array = _arrayStorage; + var values = new object[_count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = array[i].Value; + } + + return values; + } + } + + IEnumerable IReadOnlyDictionary.Values => Values; + + /// + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + public void Add(string key, object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + EnsureCapacity(_count + 1); + + if (ContainsKeyArray(key)) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(key)); + } + + _arrayStorage[_count] = new KeyValuePair(key, value); + _count++; + } + + /// + public void Clear() + { + if (_count == 0) + { + return; + } + + if (_propertyStorage != null) + { + _arrayStorage = Array.Empty>(); + _propertyStorage = null; + _count = 0; + return; + } + + Array.Clear(_arrayStorage, 0, _count); + _count = 0; + } + + /// + bool ICollection>.Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); + } + + /// + public bool ContainsKey(string key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + return ContainsKeyCore(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyCore(string key) + { + if (_propertyStorage == null) + { + return ContainsKeyArray(key); + } + + return ContainsKeyProperties(key); + } + + /// + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (Count == 0) + { + return; + } + + EnsureCapacity(Count); + + var storage = _arrayStorage; + Array.Copy(storage, 0, array, arrayIndex, _count); + } + + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (Count == 0) + { + return false; + } + + EnsureCapacity(Count); + + var index = FindIndex(item.Key); + var array = _arrayStorage; + if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) + { + Array.Copy(array, index + 1, array, index, _count - index); + _count--; + array[_count] = default; + return true; + } + + return false; + } + + /// + public bool Remove(string key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (Count == 0) + { + return false; + } + + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); + + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + return false; + } + + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, contains the object removed from the , or null if key does not exist. + /// + /// true if the object was removed successfully; otherwise, false. + /// + public bool Remove(string key, out object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_count == 0) + { + value = default; + return false; + } + + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); + + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + value = array[index].Value; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + value = default; + return false; + } + + /// + /// Attempts to the add the provided and to the dictionary. + /// + /// The key. + /// The value. + /// Returns true if the value was added. Returns false if the key was already present. + public bool TryAdd(string key, object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (ContainsKeyCore(key)) + { + return false; + } + + EnsureCapacity(Count + 1); + _arrayStorage[Count] = new KeyValuePair(key, value); + _count++; + return true; + } + + /// + public bool TryGetValue(string key, out object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_propertyStorage == null) + { + return TryFindItem(key, out value); + } + + return TryGetValueSlow(key, out value); + } + + private bool TryGetValueSlow(string key, out object value) + { + if (_propertyStorage != null) + { + var storage = _propertyStorage; + for (var i = 0; i < storage.Properties.Length; i++) + { + if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) + { + value = storage.Properties[i].GetValue(storage.Value); + return true; + } + } + } + + value = default; + return false; + } + + private static void ThrowArgumentNullExceptionForKey() + { + throw new ArgumentNullException("key"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) + { + if (_propertyStorage != null || _arrayStorage.Length < capacity) + { + EnsureCapacitySlow(capacity); + } + } + + private void EnsureCapacitySlow(int capacity) + { + if (_propertyStorage != null) + { + var storage = _propertyStorage; + + // If we're converting from properties, it's likely due to an 'add' to make sure we have at least + // the default amount of space. + capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity)); + var array = new KeyValuePair[capacity]; + + for (var i = 0; i < storage.Properties.Length; i++) + { + var property = storage.Properties[i]; + array[i] = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + } + + _arrayStorage = array; + _propertyStorage = null; + return; + } + + if (_arrayStorage.Length < capacity) + { + capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2; + var array = new KeyValuePair[capacity]; + if (_count > 0) + { + Array.Copy(_arrayStorage, 0, array, 0, _count); + } + + _arrayStorage = array; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindIndex(string key) + { + // Generally the bounds checking here will be elided by the JIT because this will be called + // on the same code path as EnsureCapacity. + var array = _arrayStorage; + var count = _count; + + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryFindItem(string key, out object value) + { + var array = _arrayStorage; + var count = _count; + + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + value = array[i].Value; + return true; + } + } + } + + value = null; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyArray(string key) + { + var array = _arrayStorage; + var count = _count; + + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyProperties(string key) + { + var properties = _propertyStorage.Properties; + for (var i = 0; i < properties.Length; i++) + { + if (string.Equals(properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public struct Enumerator : IEnumerator> + { + private readonly RouteValueDictionary _dictionary; + private int _index; + + public Enumerator(RouteValueDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(); + } + + _dictionary = dictionary; + + Current = default; + _index = 0; + } + + public KeyValuePair Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + // Similar to the design of List.Enumerator - Split into fast path and slow path for inlining friendliness + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var dictionary = _dictionary; + + // The uncommon case is that the propertyStorage is in use + if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count)) + { + Current = dictionary._arrayStorage[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + var dictionary = _dictionary; + if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count)) + { + var storage = dictionary._propertyStorage; + var property = storage.Properties[_index]; + Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + _index++; + return true; + } + + _index = dictionary._count; + Current = default; + return false; + } + + public void Reset() + { + Current = default; + _index = 0; + } + } + + internal class PropertyStorage + { + private static readonly ConcurrentDictionary _propertyCache = new ConcurrentDictionary(); + + public readonly object Value; + public readonly PropertyHelper[] Properties; + + public PropertyStorage(object value) + { + Debug.Assert(value != null); + Value = value; + + // Cache the properties so we can know if we've already validated them for duplicates. + var type = Value.GetType(); + if (!_propertyCache.TryGetValue(type, out Properties)) + { + Properties = PropertyHelper.GetVisibleProperties(type); + ValidatePropertyNames(type, Properties); + _propertyCache.TryAdd(type, Properties); + } + } + + private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) + { + var names = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < properties.Length; i++) + { + var property = properties[i]; + + if (names.TryGetValue(property.Name, out var duplicate)) + { + var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( + type.FullName, + property.Name, + duplicate.Name, + nameof(RouteValueDictionary)); + throw new InvalidOperationException(message); + } + + names.Add(property.Name, property); + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs new file mode 100644 index 0000000000..b65af91f4f --- /dev/null +++ b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) .NET Foundation. 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class EndpointHttpContextExtensionsTests + { + [Fact] + public void GetEndpoint_ContextWithoutFeature_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Null(endpoint); + } + + [Fact] + public void GetEndpoint_ContextWithFeatureAndNullEndpoint_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + context.Features.Set(new EndpointFeature + { + Endpoint = null + }); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Null(endpoint); + } + + [Fact] + public void GetEndpoint_ContextWithFeatureAndEndpoint_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + var initial = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.Features.Set(new EndpointFeature + { + Endpoint = initial + }); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Equal(initial, endpoint); + } + + [Fact] + public void SetEndpoint_NullOnContextWithoutFeature_NoFeatureSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + context.SetEndpoint(null); + + // Assert + Assert.Null(context.Features.Get()); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithoutFeature_FeatureWithEndpointSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.NotNull(feature); + Assert.Equal(endpoint, feature.Endpoint); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithFeature_EndpointSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature + { + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Equal(endpoint, feature.Endpoint); + } + + [Fact] + public void SetEndpoint_NullOnContextWithFeature_NullSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature + { + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); + + // Act + context.SetEndpoint(null); + + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Null(feature.Endpoint); + } + + [Fact] + public void SetAndGetEndpoint_Roundtrip_EndpointIsRoundtrip() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + + // Act + context.SetEndpoint(initialEndpoint); + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Equal(initialEndpoint, endpoint); + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } + } +} diff --git a/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs new file mode 100644 index 0000000000..62432b7b2b --- /dev/null +++ b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. 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.Text; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class EndpointMetadataCollectionTests + { + [Fact] + public void Constructor_Enumeration_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(new List + { + 1, + 2, + 3, + }); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + + [Fact] + public void Constructor_ParamsArray_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(1, 2, 3); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs new file mode 100644 index 0000000000..ab5925e219 --- /dev/null +++ b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs @@ -0,0 +1,2175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RouteValueDictionaryTests + { + [Fact] + public void DefaultCtor_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromNull_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(null); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary() + { + { "1", 1 } + }; + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + Assert.Single(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + + var storage = Assert.IsType[]>(dict._arrayStorage); + var otherStorage = Assert.IsType[]>(other._arrayStorage); + Assert.NotSame(otherStorage, storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary(new { key = "value" }); + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + Assert.Null(dict._arrayStorage); + + var storage = dict._propertyStorage; + var otherStorage = other._propertyStorage; + Assert.Same(otherStorage, storage); + } + + public static IEnumerable IEnumerableKeyValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("Name", "James"), + new KeyValuePair("Age", 30), + new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + public static IEnumerable IEnumerableStringValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("First Name", "James"), + new KeyValuePair("Last Name", "Henrik"), + new KeyValuePair("Middle Name", "Bob") + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + [Theory] + [MemberData(nameof(IEnumerableKeyValuePairData))] + public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Address", kvp.Key); + var address = Assert.IsType
(kvp.Value); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + }, + kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); + } + + [Theory] + [MemberData(nameof(IEnumerableStringValuePairData))] + public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, + kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, + kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); + } + + [Fact] + public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromAnonymousType() + { + // Arrange + var obj = new { cool = "beans", awesome = 123 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, + kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType() + { + // Arrange + var obj = new RegularType() { CoolnessFactor = 73 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("CoolnessFactor", kvp.Key); + Assert.Equal(73, kvp.Value); + }, + kvp => + { + Assert.Equal("IsAwesome", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() + { + // Arrange + var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("IsPublic", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() + { + // Arrange + var obj = new StaticProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() + { + // Arrange + var obj = new SetterOnly() { CoolSetOnly = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() + { + // Arrange + var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("DerivedProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }, + kvp => + { + Assert.Equal("TotallySweetProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() + { + // Arrange + var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() + { + // Arrange + var obj = new IndexerProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_MixedCaseThrows() + { + // Arrange + var obj = new { controller = "Home", Controller = "Home" }; + + var message = + $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + + $"case-insensitive comparisons."; + + // Act & Assert + var exception = Assert.Throws(() => + { + var dictionary = new RouteValueDictionary(obj); + }); + + // Ignoring case to make sure we're not testing reflection's ordering. + Assert.Equal(message, exception.Message, ignoreCase: true); + } + + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); + } + + // Our comparer is hardcoded to be IsReadOnly==false no matter what. + [Fact] + public void IsReadOnly_False() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = ((ICollection>)dict).IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void IndexGet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict[""]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict[""] = "foo"; + + // Assert + Assert.Equal("foo", dict[""]); + } + + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["kEy"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Count_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Count_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Empty(keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var values = dict.Values; + + // Assert + Assert.Empty(values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("", "foo"); + + // Assert + Assert.Equal("foo", dict[""]); + } + + [Fact] + public void Add_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + + // The upgrade from property -> array should make space for at least 4 entries + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("age", 30), kvp), + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_KeyValuePair() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + ((ICollection>)dict).Add(new KeyValuePair("key", "value")); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Clear_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + } + + [Fact] + public void Clear_PropertyStorage_AlreadyEmpty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + } + + [Fact] + public void Clear_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.Null(dict._propertyStorage); + Assert.Empty(dict._arrayStorage); + } + + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void Contains_ListStorage_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_ListStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_ListStorage_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_ListStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_PropertyStorage_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void Contains_PropertyStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void ContainsKey_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey(""); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void CopyTo() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var array = new KeyValuePair[2]; + + // Act + ((ICollection>)dict).CopyTo(array, 1); + + // Assert + Assert.Equal( + new KeyValuePair[] + { + default(KeyValuePair), + new KeyValuePair("key", "value") + }, + array); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Remove_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove(""); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Remove_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + + [Fact] + public void Remove_KeyAndOutValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value } + }; + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value }, + { "other", 5 }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "other", 5 }, + { "key", value }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "other", 5 }, + { "dotnet", "rocks" }, + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryAdd_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryAdd("", "foo"); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryAdd_PropertyStorage_KeyDoesNotExist_ConvertsPropertyStorageToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("otherKey", "value"); + + // Assert + Assert.True(result); + Assert.Null(dict._propertyStorage); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(new KeyValuePair("otherKey", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_PropertyStory_KeyExist_DoesNotConvertPropertyStorageToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.False(result); + Assert.Null(dict._arrayStorage); + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void TryAdd_EmptyStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key1", "value1"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_CanAddWithResize() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + + // Act + var result = dict.TryAdd("key4", "value4"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(new KeyValuePair("key2", "value2"), kvp), + kvp => Assert.Equal(new KeyValuePair("key3", "value3"), kvp), + kvp => Assert.Equal(new KeyValuePair("key4", "value4"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key0", "value1"); + + // Assert + Assert.False(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryGetValue("", out var value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ListStorage_DynamicallyAdjustsCapacity() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act 1 + dict.Add("key", "value"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(4, storage.Length); + + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(8, storage.Length); + } + + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new RouteValueDictionary(); + dict.Add("key", "value"); + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(3, dict.Count); + + // Act + dict.Remove("key2"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(2, dict.Count); + Assert.Equal("key", storage[0].Key); + Assert.Equal("value", storage[0].Value); + Assert.Equal("key3", storage[1].Key); + Assert.Equal("value3", storage[1].Value); + } + + [Fact] + public void FromArray_TakesOwnershipOfArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair("a", 0), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + }; + + var dictionary = RouteValueDictionary.FromArray(array); + + // Act - modifying the array should modify the dictionary + array[0] = new KeyValuePair("aa", 10); + + // Assert + Assert.Equal(3, dictionary.Count); + Assert.Equal(10, dictionary["aa"]); + } + + [Fact] + public void FromArray_EmptyArray() + { + // Arrange + var array = Array.Empty>(); + + // Act + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void FromArray_RemovesGapsInArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair(null, null), + new KeyValuePair("a", 0), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + new KeyValuePair("d", 3), + new KeyValuePair(null, null), + }; + + // Act - calling From should modify the array + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Equal(4, dictionary.Count); + Assert.Equal( + new KeyValuePair[] + { + new KeyValuePair("d", 3), + new KeyValuePair("a", 0), + new KeyValuePair("c", 2), + new KeyValuePair("b", 1), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + }, + array); + } + + private class RegularType + { + public bool IsAwesome { get; set; } + + public int CoolnessFactor { get; set; } + } + + private class Visibility + { + private string PrivateYo { get; set; } + + internal int ItsInternalDealWithIt { get; set; } + + public bool IsPublic { get; set; } + } + + private class StaticProperty + { + public static bool IsStatic { get; set; } + } + + private class SetterOnly + { + private bool _coolSetOnly; + + public bool CoolSetOnly { set { _coolSetOnly = value; } } + } + + private class Base + { + public bool DerivedProperty { get; set; } + } + + private class Derived : Base + { + public bool TotallySweetProperty { get; set; } + } + + private class DerivedHiddenProperty : Base + { + public new int DerivedProperty { get; set; } + } + + private class IndexerProperty + { + public bool this[string key] + { + get { return false; } + set { } + } + } + + private class Address + { + public string City { get; set; } + + public string State { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs index 1723ee6fd5..70f1c4da71 100644 --- a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs +++ b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http if (value.HasValue) { - headers[name] = HeaderUtilities.FormatDate(value.Value); + headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault()); } else { diff --git a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs index 74c0422ef4..90282bd2df 100644 --- a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs +++ b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Http throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); } if (count.HasValue && - (count.Value < 0 || count.Value > fileLength - offset)) + (count.GetValueOrDefault() < 0 || count.GetValueOrDefault() > fileLength - offset)) { throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); } diff --git a/src/Http/Http.Extensions/src/StreamCopyOperation.cs b/src/Http/Http.Extensions/src/StreamCopyOperation.cs index 12067fef65..e0a301f4eb 100644 --- a/src/Http/Http.Extensions/src/StreamCopyOperation.cs +++ b/src/Http/Http.Extensions/src/StreamCopyOperation.cs @@ -42,13 +42,13 @@ namespace Microsoft.AspNetCore.Http.Extensions { Debug.Assert(source != null); Debug.Assert(destination != null); - Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0); Debug.Assert(buffer != null); while (true) { // The natural end of the range. - if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0) { return; } @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Http.Extensions int readLength = buffer.Length; if (bytesRemaining.HasValue) { - readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength); } int read = await source.ReadAsync(buffer, 0, readLength, cancel); diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 633e591186..a6b5846a7d 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -195,17 +195,18 @@ namespace Microsoft.AspNetCore.Http.Extensions /// public static string GetDisplayUrl(this HttpRequest request) { - var host = request.Host.Value; - var pathBase = request.PathBase.Value; - var path = request.Path.Value; - var queryString = request.QueryString.Value; + var scheme = request.Scheme ?? string.Empty; + var host = request.Host.Value ?? string.Empty; + var pathBase = request.PathBase.Value ?? string.Empty; + var path = request.Path.Value ?? string.Empty; + var queryString = request.QueryString.Value ?? string.Empty; // PERF: Calculate string length to allocate correct buffer size for StringBuilder. - var length = request.Scheme.Length + SchemeDelimiter.Length + host.Length + var length = scheme.Length + SchemeDelimiter.Length + host.Length + pathBase.Length + path.Length + queryString.Length; return new StringBuilder(length) - .Append(request.Scheme) + .Append(scheme) .Append(SchemeDelimiter) .Append(host) .Append(pathBase) diff --git a/src/Http/Http.Extensions/test/UriHelperTests.cs b/src/Http/Http.Extensions/test/UriHelperTests.cs index 11b045af4f..ba604d576e 100644 --- a/src/Http/Http.Extensions/test/UriHelperTests.cs +++ b/src/Http/Http.Extensions/test/UriHelperTests.cs @@ -55,17 +55,19 @@ namespace Microsoft.AspNetCore.Http.Extensions Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); } - [Fact] - public void GetDisplayUrlFromRequest() + [Theory] + [InlineData("/un?escaped/base")] + [InlineData(null)] + public void GetDisplayUrlFromRequest(string pathBase) { var request = new DefaultHttpContext().Request; request.Scheme = "http"; request.Host = new HostString("my.HoΨst:80"); - request.PathBase = new PathString("/un?escaped/base"); + request.PathBase = new PathString(pathBase); request.Path = new PathString("/un?escaped"); request.QueryString = new QueryString("?name=val%23ue"); - Assert.Equal("http://my.hoψst:80/un?escaped/base/un?escaped?name=val%23ue", request.GetDisplayUrl()); + Assert.Equal("http://my.hoψst:80" + pathBase + "/un?escaped?name=val%23ue", request.GetDisplayUrl()); } [Theory] diff --git a/src/Http/Http.Features/src/IFormFileCollection.cs b/src/Http/Http.Features/src/IFormFileCollection.cs index e66c96e05d..ab862c917b 100644 --- a/src/Http/Http.Features/src/IFormFileCollection.cs +++ b/src/Http/Http.Features/src/IFormFileCollection.cs @@ -10,10 +10,33 @@ namespace Microsoft.AspNetCore.Http /// public interface IFormFileCollection : IReadOnlyList { + /// + /// Gets the first file with the specified name. + /// + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// IFormFile this[string name] { get; } + /// + /// Gets the first file with the specified name. + /// + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// IFormFile GetFile(string name); + /// + /// Gets an containing the files of the + /// with the specified name. + /// + /// The name of the files to get. + /// + /// An containing the files of the object + /// that implements . + /// IReadOnlyList GetFiles(string name); } -} \ No newline at end of file +} diff --git a/src/Http/Http/perf/Microsoft.AspNetCore.Http.Performance.csproj b/src/Http/Http/perf/Microsoft.AspNetCore.Http.Performance.csproj new file mode 100644 index 0000000000..8f1fed848b --- /dev/null +++ b/src/Http/Http/perf/Microsoft.AspNetCore.Http.Performance.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.2 + Exe + true + true + false + Microsoft.AspNetCore.Http + + + + + + + + + diff --git a/src/Http/Http/perf/Properties/AssemblyInfo.cs b/src/Http/Http/perf/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2efc4cb5fb --- /dev/null +++ b/src/Http/Http/perf/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] \ No newline at end of file diff --git a/src/Http/Http/perf/RouteValueDictionaryBenchmark.cs b/src/Http/Http/perf/RouteValueDictionaryBenchmark.cs new file mode 100644 index 0000000000..2dfc36afa4 --- /dev/null +++ b/src/Http/Http/perf/RouteValueDictionaryBenchmark.cs @@ -0,0 +1,325 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteValueDictionaryBenchmark + { + private RouteValueDictionary _arrayValues; + private RouteValueDictionary _propertyValues; + private RouteValueDictionary _arrayValuesEmpty; + + // We modify the route value dictionaries in many of these benchmarks. + [IterationSetup] + public void Setup() + { + _arrayValues = new RouteValueDictionary() + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + _arrayValuesEmpty = new RouteValueDictionary(); + _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + } + + [Benchmark] + public void Ctor_Values_RouteValueDictionary_EmptyArray() + { + new RouteValueDictionary(_arrayValuesEmpty); + } + + [Benchmark] + public void Ctor_Values_RouteValueDictionary_Array() + { + new RouteValueDictionary(_arrayValues); + } + + [Benchmark] + public RouteValueDictionary AddSingleItem() + { + var dictionary = new RouteValueDictionary + { + { "action", "Index" } + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary AddThreeItems() + { + var dictionary = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "15" } + }; + return dictionary; + } + + [Benchmark] + public void ContainsKey_Array_Found() + { + _arrayValues.ContainsKey("id"); + } + + [Benchmark] + public void ContainsKey_Array_NotFound() + { + _arrayValues.ContainsKey("name"); + } + + [Benchmark] + public void ContainsKey_Properties_Found() + { + _propertyValues.ContainsKey("id"); + } + + [Benchmark] + public void ContainsKey_Properties_NotFound() + { + _propertyValues.ContainsKey("name"); + } + + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + propertyValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + _propertyValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + propertyValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + _propertyValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + { "area", "root" } + }; + arrayValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + { "area", "root" } + }; + arrayValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" } + }; + arrayValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + arrayValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void ConditionalAdd_Array() + { + var arrayValues = new RouteValueDictionary() + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + + if (!arrayValues.ContainsKey("name")) + { + arrayValues.Add("name", "Service"); + } + } + + [Benchmark] + public void ConditionalAdd_Properties() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + + if (!propertyValues.ContainsKey("name")) + { + propertyValues.Add("name", "Service"); + } + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_ContainsKey_Array() + { + var dictionary = _arrayValues; + + if (!dictionary.ContainsKey("action")) + { + dictionary.Add("action", "Index"); + } + + if (!dictionary.ContainsKey("controller")) + { + dictionary.Add("controller", "Home"); + } + + if (!dictionary.ContainsKey("area")) + { + dictionary.Add("area", "Admin"); + } + + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_TryAdd() + { + var dictionary = _arrayValues; + + dictionary.TryAdd("action", "Index"); + dictionary.TryAdd("controller", "Home"); + dictionary.TryAdd("area", "Admin"); + + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Array() + { + var dictionary = _arrayValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Properties() + { + var dictionary = _propertyValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Array() + { + var dictionary = _arrayValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Properties() + { + var dictionary = _propertyValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetSingleItem() + { + var dictionary = new RouteValueDictionary + { + ["action"] = "Index" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetExistingItem() + { + var dictionary = _arrayValues; + dictionary["action"] = "About"; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetThreeItems() + { + var dictionary = new RouteValueDictionary + { + ["action"] = "Index", + ["controller"] = "Home", + ["id"] = "15" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Array() + { + var dictionary = _arrayValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Properties() + { + var dictionary = _propertyValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + } +} diff --git a/src/Http/Http/perf/StreamPipeWriterBenchmark.cs b/src/Http/Http/perf/StreamPipeWriterBenchmark.cs new file mode 100644 index 0000000000..705cb0d8af --- /dev/null +++ b/src/Http/Http/perf/StreamPipeWriterBenchmark.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Http +{ + public class StreamPipeWriterBenchmark + { + private Stream _memoryStream; + private StreamPipeWriter _pipeWriter; + private static byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("Hello World"); + private static byte[] _largeWrite = Encoding.ASCII.GetBytes(new string('a', 50000)); + + [IterationSetup] + public void Setup() + { + _memoryStream = new NoopStream(); + _pipeWriter = new StreamPipeWriter(_memoryStream); + } + + [Benchmark] + public async Task WriteHelloWorld() + { + await _pipeWriter.WriteAsync(_helloWorldBytes); + } + + [Benchmark] + public async Task WriteHelloWorldLargeWrite() + { + await _pipeWriter.WriteAsync(_largeWrite); + } + + public class NoopStream : Stream + { + public override bool CanRead => false; + + public override bool CanSeek => throw new System.NotImplementedException(); + + public override bool CanWrite => true; + + public override long Length => throw new System.NotImplementedException(); + + public override long Position { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new System.NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new System.NotImplementedException(); + } + + public override void SetLength(long value) + { + } + + public override void Write(byte[] buffer, int offset, int count) + { + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default(CancellationToken)) + { + return default(ValueTask); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + } +} diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs index 865e183f76..d93234d08b 100644 --- a/src/Http/Http/src/Features/FormFeature.cs +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Http.Features if (section.BaseStreamOffset.HasValue) { // Relative reference to buffered request body - file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length, name, fileName); + file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName); } else { diff --git a/src/Http/Http/src/Features/RouteValuesFeature.cs b/src/Http/Http/src/Features/RouteValuesFeature.cs new file mode 100644 index 0000000000..e4a459e991 --- /dev/null +++ b/src/Http/Http/src/Features/RouteValuesFeature.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature for routing values. Use + /// to access the values associated with the current request. + /// + public class RouteValuesFeature : IRouteValuesFeature + { + private RouteValueDictionary _routeValues; + + /// + /// Gets or sets the associated with the currrent + /// request. + /// + public RouteValueDictionary RouteValues + { + get + { + if (_routeValues == null) + { + _routeValues = new RouteValueDictionary(); + } + + return _routeValues; + } + set => _routeValues = value; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/HeaderDictionary.cs b/src/Http/Http/src/HeaderDictionary.cs index bc0b7a26ce..2113204823 100644 --- a/src/Http/Http/src/HeaderDictionary.cs +++ b/src/Http/Http/src/HeaderDictionary.cs @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Http ThrowIfReadOnly(); if (value.HasValue) { - this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.Value); + this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } else { diff --git a/src/Http/Http/src/HttpContextAccessor.cs b/src/Http/Http/src/HttpContextAccessor.cs index 897c27f734..286151029c 100644 --- a/src/Http/Http/src/HttpContextAccessor.cs +++ b/src/Http/Http/src/HttpContextAccessor.cs @@ -7,21 +7,35 @@ namespace Microsoft.AspNetCore.Http { public class HttpContextAccessor : IHttpContextAccessor { - private static AsyncLocal<(string traceIdentifier, HttpContext context)> _httpContextCurrent = new AsyncLocal<(string traceIdentifier, HttpContext context)>(); + private static AsyncLocal _httpContextCurrent = new AsyncLocal(); public HttpContext HttpContext { get { - var value = _httpContextCurrent.Value; - // Only return the context if the stored request id matches the stored trace identifier - // context.TraceIdentifier is cleared by HttpContextFactory.Dispose. - return value.traceIdentifier == value.context?.TraceIdentifier ? value.context : null; + return _httpContextCurrent.Value?.Context; } set { - _httpContextCurrent.Value = (value?.TraceIdentifier, value); + var holder = _httpContextCurrent.Value; + if (holder != null) + { + // Clear current HttpContext trapped in the AsyncLocals, as its done. + holder.Context = null; + } + + if (value != null) + { + // Use an object indirection to hold the HttpContext in the AsyncLocal, + // so it can be cleared in all ExecutionContexts when its cleared. + _httpContextCurrent.Value = new HttpContextHolder { Context = value }; + } } } + + private class HttpContextHolder + { + public HttpContext Context; + } } } diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs index f293ef4782..8236a388a5 100644 --- a/src/Http/Http/src/HttpContextFactory.cs +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -53,10 +53,6 @@ namespace Microsoft.AspNetCore.Http { _httpContextAccessor.HttpContext = null; } - - // Null out the TraceIdentifier here as a sign that this request is done, - // the HttpContextAccessor implementation relies on this to detect that the request is over - httpContext.TraceIdentifier = null; } } } \ No newline at end of file diff --git a/src/Http/Http/src/Internal/ApplicationBuilder.cs b/src/Http/Http/src/Internal/ApplicationBuilder.cs index d0b6b6f6bf..8c7e5d1a50 100644 --- a/src/Http/Http/src/Internal/ApplicationBuilder.cs +++ b/src/Http/Http/src/Internal/ApplicationBuilder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Internal; @@ -81,6 +82,13 @@ namespace Microsoft.AspNetCore.Builder.Internal { RequestDelegate app = context => { + // Implicitly execute matched endpoint at the end of the pipeline instead of returning 404 + var endpointRequestDelegate = context.GetEndpoint()?.RequestDelegate; + if (endpointRequestDelegate != null) + { + return endpointRequestDelegate(context); + } + context.Response.StatusCode = 404; return Task.CompletedTask; }; diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs index f216475db6..e2512f60dc 100644 --- a/src/Http/Http/src/Internal/DefaultHttpRequest.cs +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http.Internal @@ -17,6 +18,7 @@ namespace Microsoft.AspNetCore.Http.Internal private readonly static Func _newQueryFeature = f => new QueryFeature(f); private readonly static Func _newFormFeature = r => new FormFeature(r); private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); private HttpContext _context; private FeatureReferences _features; @@ -52,6 +54,9 @@ namespace Microsoft.AspNetCore.Http.Internal private IRequestCookiesFeature RequestCookiesFeature => _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + private IRouteValuesFeature RouteValuesFeature => + _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + public override PathString PathBase { get { return new PathString(HttpRequestFeature.PathBase); } @@ -151,12 +156,19 @@ namespace Microsoft.AspNetCore.Http.Internal return FormFeature.ReadFormAsync(cancellationToken); } + public override RouteValueDictionary RouteValues + { + get { return RouteValuesFeature.RouteValues; } + set { RouteValuesFeature.RouteValues = value; } + } + struct FeatureInterfaces { public IHttpRequestFeature Request; public IQueryFeature Query; public IFormFeature Form; public IRequestCookiesFeature Cookies; + public IRouteValuesFeature RouteValues; } } } \ No newline at end of file diff --git a/src/Http/Http/src/Internal/FormFileCollection.cs b/src/Http/Http/src/Internal/FormFileCollection.cs index 806e756a8e..bb13e121b2 100644 --- a/src/Http/Http/src/Internal/FormFileCollection.cs +++ b/src/Http/Http/src/Internal/FormFileCollection.cs @@ -6,10 +6,15 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Http.Internal { + /// + /// Default implementation of . + /// public class FormFileCollection : List, IFormFileCollection { + /// public IFormFile this[string name] => GetFile(name); + /// public IFormFile GetFile(string name) { foreach (var file in this) @@ -23,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Internal return null; } + /// public IReadOnlyList GetFiles(string name) { var files = new List(); diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 4344d0ae8e..1359ecf6e9 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -2,7 +2,7 @@ ASP.NET Core default HTTP feature implementations. - netstandard2.0 + netstandard2.0;netcoreapp2.2 $(NoWarn);CS1591 true true @@ -16,6 +16,7 @@ + diff --git a/src/Http/Http/src/StreamPipeWriter.cs b/src/Http/Http/src/StreamPipeWriter.cs new file mode 100644 index 0000000000..f232aa97cf --- /dev/null +++ b/src/Http/Http/src/StreamPipeWriter.cs @@ -0,0 +1,320 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Implements PipeWriter using a underlying stream. + /// + public class StreamPipeWriter : PipeWriter, IDisposable + { + private readonly int _minimumSegmentSize; + private readonly Stream _writingStream; + private int _bytesWritten; + + private List _completedSegments; + private Memory _currentSegment; + private IMemoryOwner _currentSegmentOwner; + private MemoryPool _pool; + private int _position; + + private CancellationTokenSource _internalTokenSource; + private bool _isCompleted; + private ExceptionDispatchInfo _exceptionInfo; + private object _lockObject = new object(); + + private CancellationTokenSource InternalTokenSource + { + get + { + lock (_lockObject) + { + if (_internalTokenSource == null) + { + _internalTokenSource = new CancellationTokenSource(); + } + return _internalTokenSource; + } + } + } + + /// + /// Creates a new StreamPipeWrapper + /// + /// The stream to write to + public StreamPipeWriter(Stream writingStream) : this(writingStream, 4096) + { + } + + public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool pool = null) + { + _minimumSegmentSize = minimumSegmentSize; + _writingStream = writingStream; + _pool = pool ?? MemoryPool.Shared; + } + + /// + public override void Advance(int count) + { + if (_currentSegment.IsEmpty) // TODO confirm this + { + throw new InvalidOperationException("No writing operation. Make sure GetMemory() was called."); + } + + if (count >= 0) + { + if (_currentSegment.Length < _position + count) + { + throw new InvalidOperationException("Can't advance past buffer size."); + } + _bytesWritten += count; + _position += count; + } + } + + /// + public override Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + + return _currentSegment; + } + + /// + public override Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + + return _currentSegment.Span.Slice(_position); + } + + /// + public override void CancelPendingFlush() + { + Cancel(); + } + + /// + public override void Complete(Exception exception = null) + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + if (exception != null) + { + _exceptionInfo = ExceptionDispatchInfo.Capture(exception); + } + + _internalTokenSource?.Dispose(); + + if (_completedSegments != null) + { + foreach (var segment in _completedSegments) + { + segment.Return(); + } + } + + _currentSegmentOwner?.Dispose(); + } + + /// + public override void OnReaderCompleted(Action callback, object state) + { + throw new NotSupportedException("OnReaderCompleted isn't supported in StreamPipeWrapper."); + } + + /// + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_bytesWritten == 0) + { + return new ValueTask(new FlushResult(isCanceled: false, IsCompletedOrThrow())); + } + + return FlushAsyncInternal(cancellationToken); + } + + private void Cancel() + { + InternalTokenSource.Cancel(); + } + + private async ValueTask FlushAsyncInternal(CancellationToken cancellationToken = default) + { + // Write all completed segments and whatever remains in the current segment + // and flush the result. + CancellationTokenRegistration reg = new CancellationTokenRegistration(); + if (cancellationToken.CanBeCanceled) + { + reg = cancellationToken.Register(state => ((StreamPipeWriter)state).Cancel(), this); + } + using (reg) + { + var localToken = InternalTokenSource.Token; + try + { + if (_completedSegments != null && _completedSegments.Count > 0) + { + var count = _completedSegments.Count; + for (var i = 0; i < count; i++) + { + var segment = _completedSegments[0]; +#if NETCOREAPP2_2 + await _writingStream.WriteAsync(segment.Buffer.Slice(0, segment.Length), localToken); +#elif NETSTANDARD2_0 + MemoryMarshal.TryGetArray(segment.Buffer, out var arraySegment); + await _writingStream.WriteAsync(arraySegment.Array, 0, segment.Length, localToken); +#else +#error Target frameworks need to be updated. +#endif + _bytesWritten -= segment.Length; + segment.Return(); + _completedSegments.RemoveAt(0); + } + } + + if (!_currentSegment.IsEmpty) + { +#if NETCOREAPP2_2 + await _writingStream.WriteAsync(_currentSegment.Slice(0, _position), localToken); +#elif NETSTANDARD2_0 + MemoryMarshal.TryGetArray(_currentSegment, out var arraySegment); + await _writingStream.WriteAsync(arraySegment.Array, 0, _position, localToken); +#else +#error Target frameworks need to be updated. +#endif + _bytesWritten -= _position; + _position = 0; + } + + await _writingStream.FlushAsync(localToken); + + return new FlushResult(isCanceled: false, IsCompletedOrThrow()); + } + catch (OperationCanceledException) + { + // Remove the cancellation token such that the next time Flush is called + // A new CTS is created. + lock (_lockObject) + { + _internalTokenSource = null; + } + + if (cancellationToken.IsCancellationRequested) + { + throw; + } + + // Catch any cancellation and translate it into setting isCanceled = true + return new FlushResult(isCanceled: true, IsCompletedOrThrow()); + } + } + } + + private void EnsureCapacity(int sizeHint) + { + // This does the Right Thing. It only subtracts _position from the current segment length if it's non-null. + // If _currentSegment is null, it returns 0. + var remainingSize = _currentSegment.Length - _position; + + // If the sizeHint is 0, any capacity will do + // Otherwise, the buffer must have enough space for the entire size hint, or we need to add a segment. + if ((sizeHint == 0 && remainingSize > 0) || (sizeHint > 0 && remainingSize >= sizeHint)) + { + // We have capacity in the current segment + return; + } + + AddSegment(sizeHint); + } + + private void AddSegment(int sizeHint = 0) + { + if (_currentSegment.Length != 0) + { + // We're adding a segment to the list + if (_completedSegments == null) + { + _completedSegments = new List(); + } + + // Position might be less than the segment length if there wasn't enough space to satisfy the sizeHint when + // GetMemory was called. In that case we'll take the current segment and call it "completed", but need to + // ignore any empty space in it. + _completedSegments.Add(new CompletedBuffer(_currentSegmentOwner, _position)); + } + + // Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment. + _currentSegmentOwner = _pool.Rent(Math.Max(_minimumSegmentSize, sizeHint)); + _currentSegment = _currentSegmentOwner.Memory; + _position = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsCompletedOrThrow() + { + if (!_isCompleted) + { + return false; + } + + if (_exceptionInfo != null) + { + ThrowLatchedException(); + } + + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowLatchedException() + { + _exceptionInfo.Throw(); + } + + public void Dispose() + { + Complete(); + } + + /// + /// Holds a byte[] from the pool and a size value. Basically a Memory but guaranteed to be backed by an ArrayPool byte[], so that we know we can return it. + /// + private readonly struct CompletedBuffer + { + public Memory Buffer { get; } + public int Length { get; } + + public ReadOnlySpan Span => Buffer.Span; + + private readonly IMemoryOwner _memoryOwner; + + public CompletedBuffer(IMemoryOwner buffer, int length) + { + Buffer = buffer.Memory; + Length = length; + _memoryOwner = buffer; + } + + public void Return() + { + _memoryOwner.Dispose(); + } + } + } +} diff --git a/src/Http/Http/test/FlushResultCancellationTests.cs b/src/Http/Http/test/FlushResultCancellationTests.cs new file mode 100644 index 0000000000..f4ab7cb96f --- /dev/null +++ b/src/Http/Http/test/FlushResultCancellationTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. 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.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class FlushResultCancellationTests : PipeTest + { + [Fact] + public void FlushAsyncCancellationDeadlock() + { + var cts = new CancellationTokenSource(); + var cts2 = new CancellationTokenSource(); + + PipeWriter buffer = Writer.WriteEmpty(MaximumSizeHigh); + + var e = new ManualResetEventSlim(); + + ValueTaskAwaiter awaiter = buffer.FlushAsync(cts.Token).GetAwaiter(); + awaiter.OnCompleted( + () => { + // We are on cancellation thread and need to wait until another FlushAsync call + // takes pipe state lock + e.Wait(); + + // Make sure we had enough time to reach _cancellationTokenRegistration.Dispose + Thread.Sleep(100); + + // Try to take pipe state lock + buffer.FlushAsync(); + }); + + // Start a thread that would run cancellation callbacks + Task cancellationTask = Task.Run(() => cts.Cancel()); + // Start a thread that would call FlushAsync with different token + // and block on _cancellationTokenRegistration.Dispose + Task blockingTask = Task.Run( + () => { + e.Set(); + buffer.FlushAsync(cts2.Token); + }); + + bool completed = Task.WhenAll(cancellationTask, blockingTask).Wait(TimeSpan.FromSeconds(10)); + Assert.True(completed); + } + + [Fact] + public async Task FlushAsyncWithNewCancellationTokenNotAffectedByPrevious() + { + var cancellationTokenSource1 = new CancellationTokenSource(); + PipeWriter buffer = Writer.WriteEmpty(10); + await buffer.FlushAsync(cancellationTokenSource1.Token); + + cancellationTokenSource1.Cancel(); + + var cancellationTokenSource2 = new CancellationTokenSource(); + buffer = Writer.WriteEmpty(10); + + await buffer.FlushAsync(cancellationTokenSource2.Token); + } + } +} diff --git a/src/Http/Http/test/HttpContextAccessorTests.cs b/src/Http/Http/test/HttpContextAccessorTests.cs index c1521b1bc3..c224a66a7d 100644 --- a/src/Http/Http/test/HttpContextAccessorTests.cs +++ b/src/Http/Http/test/HttpContextAccessorTests.cs @@ -44,7 +44,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -76,7 +75,6 @@ namespace Microsoft.AspNetCore.Http // Null out the accessor accessor.HttpContext = null; - context.TraceIdentifier = null; waitForNullTcs.SetResult(null); @@ -86,12 +84,11 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfDifferentTraceIdentifier() + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfChanged() { var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -121,12 +118,8 @@ namespace Microsoft.AspNetCore.Http await checkAsyncFlowTcs.Task; - // Reset the trace identifier on the first request - context.TraceIdentifier = null; - // Set a new http context var context2 = new DefaultHttpContext(); - context2.TraceIdentifier = "2"; accessor.HttpContext = context2; waitForNullTcs.SetResult(null); @@ -142,7 +135,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -172,7 +164,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Http/Http/test/HttpContextFactoryTests.cs b/src/Http/Http/test/HttpContextFactoryTests.cs index 80e421273a..56b996f5be 100644 --- a/src/Http/Http/test/HttpContextFactoryTests.cs +++ b/src/Http/Http/test/HttpContextFactoryTests.cs @@ -34,7 +34,6 @@ namespace Microsoft.AspNetCore.Http // Act var context = contextFactory.Create(new FeatureCollection()); - var traceIdentifier = context.TraceIdentifier; // Assert Assert.Same(context, accessor.HttpContext); @@ -42,7 +41,6 @@ namespace Microsoft.AspNetCore.Http contextFactory.Dispose(context); Assert.Null(accessor.HttpContext); - Assert.NotEqual(traceIdentifier, context.TraceIdentifier); } [Fact] diff --git a/src/Http/Http/test/Internal/ApplicationBuilderTests.cs b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs index e1336c82ba..2c57489f91 100644 --- a/src/Http/Http/test/Internal/ApplicationBuilderTests.cs +++ b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Xunit; namespace Microsoft.AspNetCore.Builder.Internal @@ -20,6 +22,59 @@ namespace Microsoft.AspNetCore.Builder.Internal Assert.Equal(404, httpContext.Response.StatusCode); } + [Fact] + public void BuildImplicitlyCallsMatchedEndpointAsLastStep() + { + var builder = new ApplicationBuilder(null); + var app = builder.Build(); + + var endpointCalled = false; + var endpoint = new Endpoint( + context => + { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); + + app.Invoke(httpContext); + + Assert.True(endpointCalled); + } + + [Fact] + public void BuildDoesNotCallMatchedEndpointWhenTerminated() + { + var builder = new ApplicationBuilder(null); + builder.Use((context, next) => + { + // Do not call next + return Task.CompletedTask; + }); + var app = builder.Build(); + + var endpointCalled = false; + var endpoint = new Endpoint( + context => + { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); + + app.Invoke(httpContext); + + Assert.False(endpointCalled); + } + [Fact] public void PropertiesDictionaryIsDistinctAfterNew() { diff --git a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs index dbe1d54dd0..09e47a962e 100644 --- a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Xunit; @@ -194,6 +195,57 @@ namespace Microsoft.AspNetCore.Http.Internal Assert.Equal(new[] { "name2=value2" }, cookieHeaders); } + [Fact] + public void RouteValues_GetAndSet() + { + var context = new DefaultHttpContext(); + var request = context.Request; + + var routeValuesFeature = context.Features.Get(); + // No feature set for initial DefaultHttpRequest + Assert.Null(routeValuesFeature); + + // Route values returns empty collection by default + Assert.Empty(request.RouteValues); + + // Get and set value on request route values + request.RouteValues["new"] = "setvalue"; + Assert.Equal("setvalue", request.RouteValues["new"]); + + routeValuesFeature = context.Features.Get(); + // Accessing DefaultHttpRequest.RouteValues creates feature + Assert.NotNull(routeValuesFeature); + + request.RouteValues = new RouteValueDictionary(new { key = "value" }); + // Can set DefaultHttpRequest.RouteValues + Assert.NotNull(request.RouteValues); + Assert.Equal("value", request.RouteValues["key"]); + + // DefaultHttpRequest.RouteValues uses feature + Assert.Equal(routeValuesFeature.RouteValues, request.RouteValues); + + // Setting route values to null sets empty collection on request + routeValuesFeature.RouteValues = null; + Assert.Empty(request.RouteValues); + + var customRouteValuesFeature = new CustomRouteValuesFeature + { + RouteValues = new RouteValueDictionary(new { key = "customvalue" }) + }; + context.Features.Set(customRouteValuesFeature); + // Can override DefaultHttpRequest.RouteValues with custom feature + Assert.Equal(customRouteValuesFeature.RouteValues, request.RouteValues); + + // Can clear feature + context.Features.Set(null); + Assert.Empty(request.RouteValues); + } + + private class CustomRouteValuesFeature : IRouteValuesFeature + { + public RouteValueDictionary RouteValues { get; set; } + } + private static HttpRequest CreateRequest(IHeaderDictionary headers) { var context = new DefaultHttpContext(); diff --git a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj index c072fc6f67..78ac0c0ff1 100644 --- a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj +++ b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj @@ -2,6 +2,7 @@ $(StandardTestTfms) + true diff --git a/src/Http/Http/test/PipeTest.cs b/src/Http/Http/test/PipeTest.cs new file mode 100644 index 0000000000..2e94e3a267 --- /dev/null +++ b/src/Http/Http/test/PipeTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.IO.Pipelines; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public abstract class PipeTest : IDisposable + { + protected const int MaximumSizeHigh = 65; + + public MemoryStream MemoryStream { get; set; } + + public PipeWriter Writer { get; set; } + + protected PipeTest() + { + MemoryStream = new MemoryStream(); + Writer = new StreamPipeWriter(MemoryStream, 4096, new TestMemoryPool()); + } + + public void Dispose() + { + Writer.Complete(); + } + + public byte[] Read() + { + Writer.FlushAsync().GetAwaiter().GetResult(); + return ReadWithoutFlush(); + } + + public byte[] ReadWithoutFlush() + { + MemoryStream.Position = 0; + var buffer = new byte[MemoryStream.Length]; + var result = MemoryStream.Read(buffer, 0, (int)MemoryStream.Length); + return buffer; + } + } +} diff --git a/src/Http/Http/test/PipeWriterTests.cs b/src/Http/Http/test/PipeWriterTests.cs new file mode 100644 index 0000000000..0cc6dc012f --- /dev/null +++ b/src/Http/Http/test/PipeWriterTests.cs @@ -0,0 +1,221 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class PipeWriterTests : PipeTest + { + + [Theory] + [InlineData(3, -1, 0)] + [InlineData(3, 0, -1)] + [InlineData(3, 0, 4)] + [InlineData(3, 4, 0)] + [InlineData(3, -1, -1)] + [InlineData(3, 4, 4)] + public void ThrowsForInvalidParameters(int arrayLength, int offset, int length) + { + var array = new byte[arrayLength]; + for (var i = 0; i < array.Length; i++) + { + array[i] = (byte)(i + 1); + } + + Writer.Write(new Span(array, 0, 0)); + Writer.Write(new Span(array, array.Length, 0)); + + try + { + Writer.Write(new Span(array, offset, length)); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is ArgumentOutOfRangeException); + } + + Writer.Write(new Span(array, 0, array.Length)); + Assert.Equal(array, Read()); + } + + [Theory] + [InlineData(0, 3)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void CanWriteWithOffsetAndLength(int offset, int length) + { + var array = new byte[] { 1, 2, 3 }; + + Writer.Write(new Span(array, offset, length)); + + Assert.Equal(array.Skip(offset).Take(length).ToArray(), Read()); + } + + [Fact] + public void CanWriteIntoHeadlessBuffer() + { + + Writer.Write(new byte[] { 1, 2, 3 }); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanGetNewMemoryWhenSizeTooLarge() + { + var memory = Writer.GetMemory(0); + + var memoryLarge = Writer.GetMemory(10000); + + Assert.NotEqual(memory, memoryLarge); + } + + [Fact] + public void CanGetSameMemoryWhenNoAdvance() + { + var memory = Writer.GetMemory(0); + + var secondMemory = Writer.GetMemory(0); + + Assert.Equal(memory, secondMemory); + } + + [Fact] + public void CanGetNewSpanWhenNoAdvanceWhenSizeTooLarge() + { + var span = Writer.GetSpan(0); + + var secondSpan = Writer.GetSpan(8000); + + Assert.False(span.SequenceEqual(secondSpan)); + } + + [Fact] + public void CanGetSameSpanWhenNoAdvance() + { + var span = Writer.GetSpan(0); + + var secondSpan = Writer.GetSpan(0); + + Assert.True(span.SequenceEqual(secondSpan)); + } + + [Theory] + [InlineData(16, 32, 32)] + [InlineData(16, 16, 16)] + [InlineData(64, 32, 64)] + [InlineData(40, 32, 64)] // memory sizes are powers of 2. + public void CheckMinimumSegmentSizeWithGetMemory(int minimumSegmentSize, int getMemorySize, int expectedSize) + { + var writer = new StreamPipeWriter(new MemoryStream(), minimumSegmentSize); + var memory = writer.GetMemory(getMemorySize); + + Assert.Equal(expectedSize, memory.Length); + } + + [Fact] + public void CanWriteMultipleTimes() + { + + Writer.Write(new byte[] { 1 }); + Writer.Write(new byte[] { 2 }); + Writer.Write(new byte[] { 3 }); + + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanWriteOverTheBlockLength() + { + Memory memory = Writer.GetMemory(); + + IEnumerable source = Enumerable.Range(0, memory.Length).Select(i => (byte)i); + byte[] expectedBytes = source.Concat(source).Concat(source).ToArray(); + + Writer.Write(expectedBytes); + + Assert.Equal(expectedBytes, Read()); + } + + [Fact] + public void EnsureAllocatesSpan() + { + var span = Writer.GetSpan(10); + + Assert.True(span.Length >= 10); + // 0 byte Flush would not complete the reader so we complete. + Writer.Complete(); + Assert.Equal(new byte[] { }, Read()); + } + + [Fact] + public void SlicesSpanAndAdvancesAfterWrite() + { + int initialLength = Writer.GetSpan(3).Length; + + + Writer.Write(new byte[] { 1, 2, 3 }); + Span span = Writer.GetSpan(); + + Assert.Equal(initialLength - 3, span.Length); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataBinary(int length) + { + var data = new byte[length]; + new Random(length).NextBytes(data); + PipeWriter output = Writer; + output.Write(data); + await output.FlushAsync(); + + var input = Read(); + Assert.Equal(data, input.ToArray()); + } + + [Fact] + public async Task CanWriteNothingToBuffer() + { + Writer.GetMemory(0); + Writer.Advance(0); // doing nothing, the hard way + await Writer.FlushAsync(); + } + + [Fact] + public void EmptyWriteDoesNotThrow() + { + Writer.Write(new byte[0]); + } + + [Fact] + public void ThrowsOnAdvanceOverMemorySize() + { + Memory buffer = Writer.GetMemory(1); + var exception = Assert.Throws(() => Writer.Advance(buffer.Length + 1)); + Assert.Equal("Can't advance past buffer size.", exception.Message); + } + + [Fact] + public void ThrowsOnAdvanceWithNoMemory() + { + PipeWriter buffer = Writer; + var exception = Assert.Throws(() => buffer.Advance(1)); + Assert.Equal("No writing operation. Make sure GetMemory() was called.", exception.Message); + } + } +} diff --git a/src/Http/Http/test/StreamPipeWriterTests.cs b/src/Http/Http/test/StreamPipeWriterTests.cs new file mode 100644 index 0000000000..76d3b34fae --- /dev/null +++ b/src/Http/Http/test/StreamPipeWriterTests.cs @@ -0,0 +1,380 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class StreamPipeWriterTests : PipeTest + { + [Fact] + public async Task CanWriteAsyncMultipleTimesIntoSameBlock() + { + + await Writer.WriteAsync(new byte[] { 1 }); + await Writer.WriteAsync(new byte[] { 2 }); + await Writer.WriteAsync(new byte[] { 3 }); + + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Theory] + [InlineData(100, 1000)] + [InlineData(100, 8000)] + [InlineData(100, 10000)] + [InlineData(8000, 100)] + [InlineData(8000, 8000)] + public async Task CanAdvanceWithPartialConsumptionOfFirstSegment(int firstWriteLength, int secondWriteLength) + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes("a")); + + var expectedLength = firstWriteLength + secondWriteLength + 1; + + var memory = Writer.GetMemory(firstWriteLength); + Writer.Advance(firstWriteLength); + + memory = Writer.GetMemory(secondWriteLength); + Writer.Advance(secondWriteLength); + + await Writer.FlushAsync(); + + Assert.Equal(expectedLength, Read().Length); + } + + [Fact] + public async Task ThrowsOnCompleteAndWrite() + { + Writer.Complete(new InvalidOperationException("Whoops")); + var exception = await Assert.ThrowsAsync(async () => await Writer.FlushAsync()); + + Assert.Equal("Whoops", exception.Message); + } + + [Fact] + public async Task WriteCanBeCancelledViaProvidedCancellationToken() + { + var pipeWriter = new StreamPipeWriter(new HangingStream()); + var cts = new CancellationTokenSource(1); + await Assert.ThrowsAsync(async () => await pipeWriter.WriteAsync(Encoding.ASCII.GetBytes("data"), cts.Token)); + } + + [Fact] + public async Task WriteCanBeCanceledViaCancelPendingFlushWhenFlushIsAsync() + { + var pipeWriter = new StreamPipeWriter(new HangingStream()); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + var writingTask = pipeWriter.WriteAsync(Encoding.ASCII.GetBytes("data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + pipeWriter.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + [Fact] + public void FlushAsyncCompletedAfterPreCancellation() + { + PipeWriter writableBuffer = Writer.WriteEmpty(1); + + Writer.CancelPendingFlush(); + + ValueTask flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + + FlushResult flushResult = flushAsync.GetAwaiter().GetResult(); + + Assert.True(flushResult.IsCanceled); + + flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + } + + [Fact] + public void FlushAsyncReturnsCanceledIfCanceledBeforeFlush() + { + CheckCanceledFlush(); + } + + [Fact] + public void FlushAsyncReturnsCanceledIfCanceledBeforeFlushMultipleTimes() + { + for (var i = 0; i < 10; i++) + { + CheckCanceledFlush(); + } + } + + [Fact] + public async Task FlushAsyncReturnsCanceledInterleaved() + { + for (var i = 0; i < 5; i++) + { + CheckCanceledFlush(); + await CheckWriteIsNotCanceled(); + } + } + + [Fact] + public async Task CancelPendingFlushBetweenWritesAllDataIsPreserved() + { + MemoryStream = new SingleWriteStream(); + Writer = new StreamPipeWriter(MemoryStream); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + + var writingTask = Writer.WriteAsync(Encoding.ASCII.GetBytes(" data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + + await Writer.WriteAsync(Encoding.ASCII.GetBytes(" more data")); + Assert.Equal(Encoding.ASCII.GetBytes("data data more data"), Read()); + } + + [Fact] + public async Task CancelPendingFlushAfterAllWritesAllDataIsPreserved() + { + MemoryStream = new CannotFlushStream(); + Writer = new StreamPipeWriter(MemoryStream); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + // Create two Segments + // First one will succeed to write, other one will hang. + var writingTask = Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + [Fact] + public async Task CancelPendingFlushLostOfCancellationsNoDataLost() + { + var writeSize = 16; + var singleWriteStream = new SingleWriteStream(); + MemoryStream = singleWriteStream; + Writer = new StreamPipeWriter(MemoryStream, minimumSegmentSize: writeSize); + + for (var i = 0; i < 10; i++) + { + FlushResult flushResult = new FlushResult(); + var expectedData = Encoding.ASCII.GetBytes(new string('a', writeSize)); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // TaskCreationOptions.RunAsync + + var task = Task.Run(async () => + { + try + { + // Create two Segments + // First one will succeed to write, other one will hang. + for (var j = 0; j < 2; j++) + { + Writer.Write(expectedData); + } + + var flushTask = Writer.FlushAsync(); + tcs.SetResult(0); + flushResult = await flushTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + // Only half of the data was written because every other flush failed. + Assert.Equal(16 * 10, ReadWithoutFlush().Length); + + // Start allowing all writes to make read succeed. + singleWriteStream.AllowAllWrites = true; + + Assert.Equal(16 * 10 * 2, Read().Length); + } + + private async Task CheckWriteIsNotCanceled() + { + var flushResult = await Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + Assert.False(flushResult.IsCanceled); + } + + private void CheckCanceledFlush() + { + PipeWriter writableBuffer = Writer.WriteEmpty(MaximumSizeHigh); + + Writer.CancelPendingFlush(); + + ValueTask flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + FlushResult flushResult = flushAsync.GetAwaiter().GetResult(); + Assert.True(flushResult.IsCanceled); + } + } + + internal class HangingStream : MemoryStream + { + + public HangingStream() + { + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + return 0; + } + } + + internal class SingleWriteStream : MemoryStream + { + private bool _shouldNextWriteFail; + + public bool AllowAllWrites { get; set; } + + +#if NETCOREAPP2_2 + public override async ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + try + { + if (_shouldNextWriteFail && !AllowAllWrites) + { + await Task.Delay(30000, cancellationToken); + } + else + { + await base.WriteAsync(source, cancellationToken); + } + } + finally + { + _shouldNextWriteFail = !_shouldNextWriteFail; + } + } +#endif + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + if (_shouldNextWriteFail && !AllowAllWrites) + { + await Task.Delay(30000, cancellationToken); + } + await base.WriteAsync(buffer, offset, count, cancellationToken); + } + finally + { + _shouldNextWriteFail = !_shouldNextWriteFail; + } + } + } + + internal class CannotFlushStream : MemoryStream + { + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + } + + internal static class TestWriterExtensions + { + public static PipeWriter WriteEmpty(this PipeWriter Writer, int count) + { + Writer.GetSpan(count).Slice(0, count).Fill(0); + Writer.Advance(count); + return Writer; + } + } +} diff --git a/src/Http/Http/test/TestMemoryPool.cs b/src/Http/Http/test/TestMemoryPool.cs new file mode 100644 index 0000000000..c5dd647dd1 --- /dev/null +++ b/src/Http/Http/test/TestMemoryPool.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class TestMemoryPool : MemoryPool + { + private MemoryPool _pool = Shared; + + private bool _disposed; + + public override IMemoryOwner Rent(int minBufferSize = -1) + { + CheckDisposed(); + return new PooledMemory(_pool.Rent(minBufferSize), this); + } + + protected override void Dispose(bool disposing) + { + _disposed = true; + } + + public override int MaxBufferSize => 4096; + + internal void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TestMemoryPool)); + } + } + + private class PooledMemory : MemoryManager + { + private IMemoryOwner _owner; + + private readonly TestMemoryPool _pool; + + private int _referenceCount; + + private bool _returned; + + private string _leaser; + + public PooledMemory(IMemoryOwner owner, TestMemoryPool pool) + { + _owner = owner; + _pool = pool; + _leaser = Environment.StackTrace; + _referenceCount = 1; + } + + ~PooledMemory() + { + Debug.Assert(_returned, "Block being garbage collected instead of returned to pool" + Environment.NewLine + _leaser); + } + + protected override void Dispose(bool disposing) + { + _pool.CheckDisposed(); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + _pool.CheckDisposed(); + Interlocked.Increment(ref _referenceCount); + + if (!MemoryMarshal.TryGetArray(_owner.Memory, out ArraySegment segment)) + { + throw new InvalidOperationException(); + } + + unsafe + { + try + { + if ((uint)elementIndex > (uint)segment.Count) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + GCHandle handle = GCHandle.Alloc(segment.Array, GCHandleType.Pinned); + + return new MemoryHandle(Unsafe.Add(((void*)handle.AddrOfPinnedObject()), elementIndex + segment.Offset), handle, this); + } + catch + { + Unpin(); + throw; + } + } + } + + public override void Unpin() + { + _pool.CheckDisposed(); + + int newRefCount = Interlocked.Decrement(ref _referenceCount); + + if (newRefCount < 0) + throw new InvalidOperationException(); + + if (newRefCount == 0) + { + _returned = true; + } + } + + protected override bool TryGetArray(out ArraySegment segment) + { + _pool.CheckDisposed(); + return MemoryMarshal.TryGetArray(_owner.Memory, out segment); + } + + public override Memory Memory + { + get + { + _pool.CheckDisposed(); + return _owner.Memory; + } + } + + public override Span GetSpan() + { + _pool.CheckDisposed(); + return _owner.Memory.Span; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs index b31c7e9790..c590a86789 100644 --- a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Owin { if (value.HasValue) { - Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.Value); + Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } else { diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs index 7952bd34b2..e1c4f642c8 100644 --- a/src/Http/WebUtilities/src/MultipartReaderStream.cs +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -151,9 +151,9 @@ namespace Microsoft.AspNetCore.WebUtilities if (_observedLength < _position) { _observedLength = _position; - if (LengthLimit.HasValue && _observedLength > LengthLimit.Value) + if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()) { - throw new InvalidDataException($"Multipart body length limit {LengthLimit.Value} exceeded."); + throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded."); } } return read; diff --git a/src/Http/WebUtilities/src/StreamHelperExtensions.cs b/src/Http/WebUtilities/src/StreamHelperExtensions.cs index e2c16a9cf2..f4ec558aaa 100644 --- a/src/Http/WebUtilities/src/StreamHelperExtensions.cs +++ b/src/Http/WebUtilities/src/StreamHelperExtensions.cs @@ -34,9 +34,9 @@ namespace Microsoft.AspNetCore.WebUtilities { // Not all streams support cancellation directly. cancellationToken.ThrowIfCancellationRequested(); - if (limit.HasValue && limit.Value - total < read) + if (limit.HasValue && limit.GetValueOrDefault() - total < read) { - throw new InvalidDataException($"The stream exceeded the data limit {limit.Value}."); + throw new InvalidDataException($"The stream exceeded the data limit {limit.GetValueOrDefault()}."); } total += read; read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);