Merge the master branch of aspnet/HttpAbstractions
This commit is contained in:
commit
ee1e2d6173
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
namespace Microsoft.AspNetCore.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Name/Value representing an token.
|
||||
/// Name/Value representing a token.
|
||||
/// </summary>
|
||||
public class AuthenticationToken
|
||||
{
|
||||
|
|
@ -19,4 +19,4 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = <any CHAR except CTLs or ";">
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<SetCookieHeaderValue> cookies, string[] input)
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RequestProtocol.
|
||||
/// Gets or sets the request protocol (e.g. HTTP/1.1).
|
||||
/// </summary>
|
||||
/// <returns>The RequestProtocol.</returns>
|
||||
/// <returns>The request protocol.</returns>
|
||||
public abstract string Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,5 +118,11 @@ namespace Microsoft.AspNetCore.Http
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of route values for this request.
|
||||
/// </summary>
|
||||
/// <returns>The collection of route values for this request.</returns>
|
||||
public virtual RouteValueDictionary RouteValues { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
|
|||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Features" />
|
||||
<Reference Include="Microsoft.Extensions.ActivatorUtilities.Sources" PrivateAssets="All" />
|
||||
<Reference Include="Microsoft.Extensions.PropertyHelper.Sources" PrivateAssets="All" />
|
||||
<Reference Include="System.Text.Encodings.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions
|
|||
internal static string FormatArgumentCannotBeNullOrEmpty()
|
||||
=> GetString("ArgumentCannotBeNullOrEmpty");
|
||||
|
||||
/// <summary>
|
||||
/// An element with the key '{0}' already exists in the {1}.
|
||||
/// </summary>
|
||||
internal static string RouteValueDictionary_DuplicateKey
|
||||
{
|
||||
get => GetString("RouteValueDictionary_DuplicateKey");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An element with the key '{0}' already exists in the {1}.
|
||||
/// </summary>
|
||||
internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
|
||||
/// </summary>
|
||||
internal static string RouteValueDictionary_DuplicatePropertyName
|
||||
{
|
||||
get => GetString("RouteValueDictionary_DuplicatePropertyName");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -156,4 +156,10 @@
|
|||
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
||||
<value>Argument cannot be null or empty.</value>
|
||||
</data>
|
||||
<data name="RouteValueDictionary_DuplicateKey" xml:space="preserve">
|
||||
<value>An element with the key '{0}' already exists in the {1}.</value>
|
||||
</data>
|
||||
<data name="RouteValueDictionary_DuplicatePropertyName" xml:space="preserve">
|
||||
<value>The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Respresents a logical endpoint in an application.
|
||||
/// </summary>
|
||||
public class Endpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="Endpoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
|
||||
/// <param name="metadata">
|
||||
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
|
||||
/// </param>
|
||||
/// <param name="displayName">
|
||||
/// The informational display name of the endpoint. May be null.
|
||||
/// </param>
|
||||
public Endpoint(
|
||||
RequestDelegate requestDelegate,
|
||||
EndpointMetadataCollection metadata,
|
||||
string displayName)
|
||||
{
|
||||
// All are allowed to be null
|
||||
RequestDelegate = requestDelegate;
|
||||
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the informational display name of this endpoint.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of metadata associated with this endpoint.
|
||||
/// </summary>
|
||||
public EndpointMetadataCollection Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate used to process requests for the endpoint.
|
||||
/// </summary>
|
||||
public RequestDelegate RequestDelegate { get; }
|
||||
|
||||
public override string ToString() => DisplayName ?? base.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods to expose Endpoint on HttpContext.
|
||||
/// </summary>
|
||||
public static class EndpointHttpContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method for getting the <see cref="Endpoint"/> for the current request.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="HttpContext"/> context.</param>
|
||||
/// <returns>The <see cref="Endpoint"/>.</returns>
|
||||
public static Endpoint GetEndpoint(this HttpContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return context.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension method for setting the <see cref="Endpoint"/> for the current request.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="HttpContext"/> context.</param>
|
||||
/// <param name="endpoint">The <see cref="Endpoint"/>.</param>
|
||||
public static void SetEndpoint(this HttpContext context, Endpoint endpoint)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var feature = context.Features.Get<IEndpointFeature>();
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of arbitrary metadata associated with an endpoint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="EndpointMetadataCollection"/> 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.
|
||||
/// </remarks>
|
||||
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
|
||||
|
||||
private readonly object[] _items;
|
||||
private readonly ConcurrentDictionary<Type, object[]> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The metadata items.</param>
|
||||
public EndpointMetadataCollection(IEnumerable<object> items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
_items = items.ToArray();
|
||||
_cache = new ConcurrentDictionary<Type, object[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The metadata items.</param>
|
||||
public EndpointMetadataCollection(params object[] items)
|
||||
: this((IEnumerable<object>)items)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item at <paramref name="index"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item to retrieve.</param>
|
||||
/// <returns>The item at <paramref name="index"/>.</returns>
|
||||
public object this[int index] => _items[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of metadata items.
|
||||
/// </summary>
|
||||
public int Count => _items.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most significant metadata item of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of metadata to retrieve.</typeparam>
|
||||
/// <returns>
|
||||
/// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T GetMetadata<T>() 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<T>();
|
||||
}
|
||||
|
||||
private T GetMetadataSlow<T>() where T : class
|
||||
{
|
||||
var array = GetOrderedMetadataSlow<T>();
|
||||
var length = array.Length;
|
||||
return length > 0 ? array[length - 1] : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata items of type <typeparamref name="T"/> in ascending
|
||||
/// order of precedence.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of metadata.</typeparam>
|
||||
/// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
|
||||
{
|
||||
if (_cache.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
return (T[])result;
|
||||
}
|
||||
|
||||
return GetOrderedMetadataSlow<T>();
|
||||
}
|
||||
|
||||
private T[] GetOrderedMetadataSlow<T>() where T : class
|
||||
{
|
||||
var items = new List<T>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
|
||||
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<object>
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element at the current position of the enumerator
|
||||
/// </summary>
|
||||
public object Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the <see cref="Enumerator"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the enumerator was successfully advanced to the next element;
|
||||
/// <c>false</c> if the enumerator has passed the end of the collection.
|
||||
/// </returns>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index < _items.Length)
|
||||
{
|
||||
Current = _items[_index++];
|
||||
return true;
|
||||
}
|
||||
|
||||
Current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enumerator to its initial position, which is before the first element in the collection.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_index = 0;
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
|
||||
/// to access an instance associated with the current request.
|
||||
/// </summary>
|
||||
public interface IEndpointFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
|
||||
/// request.
|
||||
/// </summary>
|
||||
Endpoint Endpoint { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A feature interface for routing values. Use <see cref="HttpContext.Features"/>
|
||||
/// to access the values associated with the current request.
|
||||
/// </summary>
|
||||
public interface IRouteValuesFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||
/// request.
|
||||
/// </summary>
|
||||
RouteValueDictionary RouteValues { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IDictionary{String, Object}"/> type for route values.
|
||||
/// </summary>
|
||||
public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
|
||||
{
|
||||
// 4 is a good default capacity here because that leaves enough space for area/controller/action/id
|
||||
private const int DefaultCapacity = 4;
|
||||
|
||||
internal KeyValuePair<string, object>[] _arrayStorage;
|
||||
internal PropertyStorage _propertyStorage;
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RouteValueDictionary"/> from the provided array.
|
||||
/// The new instance will take ownership of the array, and may mutate it.
|
||||
/// </summary>
|
||||
/// <param name="items">The items array.</param>
|
||||
/// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
|
||||
public static RouteValueDictionary FromArray(KeyValuePair<string, object>[] 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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty <see cref="RouteValueDictionary"/>.
|
||||
/// </summary>
|
||||
public RouteValueDictionary()
|
||||
{
|
||||
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
|
||||
/// </summary>
|
||||
/// <param name="values">An object to initialize the dictionary. The value can be of type
|
||||
/// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
|
||||
/// or an object with public properties as key-value pairs.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<string, object>[count];
|
||||
Array.Copy(other, 0, storage, 0, count);
|
||||
_arrayStorage = storage;
|
||||
_count = count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (values is IEnumerable<KeyValuePair<string, object>> keyValueEnumerable)
|
||||
{
|
||||
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||
|
||||
foreach (var kvp in keyValueEnumerable)
|
||||
{
|
||||
Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (values is IEnumerable<KeyValuePair<string, string>> stringValueEnumerable)
|
||||
{
|
||||
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||
|
||||
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<KeyValuePair<string, object>>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, object>(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_arrayStorage[index] = new KeyValuePair<string, object>(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the comparer for this dictionary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
|
||||
/// </remarks>
|
||||
public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _count;
|
||||
|
||||
/// <inheritdoc />
|
||||
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<string> 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<string> IReadOnlyDictionary<string, object>.Keys => Keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<object> 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<object> IReadOnlyDictionary<string, object>.Values => Values;
|
||||
|
||||
/// <inheritdoc />
|
||||
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
||||
{
|
||||
Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, object>(key, value);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_propertyStorage != null)
|
||||
{
|
||||
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||
_propertyStorage = null;
|
||||
_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Array.Clear(_arrayStorage, 0, _count);
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
|
||||
{
|
||||
return TryGetValue(item.Key, out var value) && EqualityComparer<object>.Default.Equals(value, item.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void ICollection<KeyValuePair<string, object>>.CopyTo(
|
||||
KeyValuePair<string, object>[] 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
|
||||
{
|
||||
if (Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureCapacity(Count);
|
||||
|
||||
var index = FindIndex(item.Key);
|
||||
var array = _arrayStorage;
|
||||
if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
|
||||
{
|
||||
Array.Copy(array, index + 1, array, index, _count - index);
|
||||
_count--;
|
||||
array[_count] = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove and return the value that has the specified key from the <see cref="RouteValueDictionary"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove and return.</param>
|
||||
/// <param name="value">When this method returns, contains the object removed from the <see cref="RouteValueDictionary"/>, or <c>null</c> if key does not exist.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the object was removed successfully; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to the add the provided <paramref name="key"/> and <paramref name="value"/> to the dictionary.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>Returns <c>true</c> if the value was added. Returns <c>false</c> if the key was already present.</returns>
|
||||
public bool TryAdd(string key, object value)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
ThrowArgumentNullExceptionForKey();
|
||||
}
|
||||
|
||||
if (ContainsKeyCore(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureCapacity(Count + 1);
|
||||
_arrayStorage[Count] = new KeyValuePair<string, object>(key, value);
|
||||
_count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, object>[capacity];
|
||||
|
||||
for (var i = 0; i < storage.Properties.Length; i++)
|
||||
{
|
||||
var property = storage.Properties[i];
|
||||
array[i] = new KeyValuePair<string, object>(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<string, object>[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<KeyValuePair<string, object>>
|
||||
{
|
||||
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<string, object> Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
// Similar to the design of List<T>.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<string, object>(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<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
|
||||
|
||||
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<string, PropertyHelper>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IEndpointFeature>(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<IEndpointFeature>(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<IEndpointFeature>());
|
||||
}
|
||||
|
||||
[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<IEndpointFeature>();
|
||||
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<IEndpointFeature>(initialFeature);
|
||||
|
||||
// Act
|
||||
var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint");
|
||||
context.SetEndpoint(endpoint);
|
||||
|
||||
// Assert
|
||||
var feature = context.Features.Get<IEndpointFeature>();
|
||||
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<IEndpointFeature>(initialFeature);
|
||||
|
||||
// Act
|
||||
context.SetEndpoint(null);
|
||||
|
||||
// Assert
|
||||
var feature = context.Features.Get<IEndpointFeature>();
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<object>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http
|
|||
|
||||
if (value.HasValue)
|
||||
{
|
||||
headers[name] = HeaderUtilities.FormatDate(value.Value);
|
||||
headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -195,17 +195,18 @@ namespace Microsoft.AspNetCore.Http.Extensions
|
|||
/// <returns></returns>
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -10,10 +10,33 @@ namespace Microsoft.AspNetCore.Http
|
|||
/// </summary>
|
||||
public interface IFormFileCollection : IReadOnlyList<IFormFile>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the first file with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the file to get.</param>
|
||||
/// <returns>
|
||||
/// The requested file, or null if it is not present.
|
||||
/// </returns>
|
||||
IFormFile this[string name] { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first file with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the file to get.</param>
|
||||
/// <returns>
|
||||
/// The requested file, or null if it is not present.
|
||||
/// </returns>
|
||||
IFormFile GetFile(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IReadOnlyList{T}" /> containing the files of the
|
||||
/// <see cref="IFormFileCollection" /> with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the files to get.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="IReadOnlyList{T}" /> containing the files of the object
|
||||
/// that implements <see cref="IFormFileCollection" />.
|
||||
/// </returns>
|
||||
IReadOnlyList<IFormFile> GetFiles(string name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Microsoft.AspNetCore.Http</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="BenchmarkDotNet" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1 @@
|
|||
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return default(ValueTask);
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A feature for routing values. Use <see cref="HttpContext.Features"/>
|
||||
/// to access the values associated with the current request.
|
||||
/// </summary>
|
||||
public class RouteValuesFeature : IRouteValuesFeature
|
||||
{
|
||||
private RouteValueDictionary _routeValues;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||
/// request.
|
||||
/// </summary>
|
||||
public RouteValueDictionary RouteValues
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_routeValues == null)
|
||||
{
|
||||
_routeValues = new RouteValueDictionary();
|
||||
}
|
||||
|
||||
return _routeValues;
|
||||
}
|
||||
set => _routeValues = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<IFeatureCollection, IQueryFeature> _newQueryFeature = f => new QueryFeature(f);
|
||||
private readonly static Func<HttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r);
|
||||
private readonly static Func<IFeatureCollection, IRequestCookiesFeature> _newRequestCookiesFeature = f => new RequestCookiesFeature(f);
|
||||
private readonly static Func<IFeatureCollection, IRouteValuesFeature> _newRouteValuesFeature = f => new RouteValuesFeature();
|
||||
|
||||
private HttpContext _context;
|
||||
private FeatureReferences<FeatureInterfaces> _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,15 @@ using System.Collections.Generic;
|
|||
|
||||
namespace Microsoft.AspNetCore.Http.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IFormFileCollection"/>.
|
||||
/// </summary>
|
||||
public class FormFileCollection : List<IFormFile>, IFormFileCollection
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IFormFile this[string name] => GetFile(name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFormFile GetFile(string name)
|
||||
{
|
||||
foreach (var file in this)
|
||||
|
|
@ -23,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Internal
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IFormFile> GetFiles(string name)
|
||||
{
|
||||
var files = new List<IFormFile>();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core default HTTP feature implementations.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<Reference Include="Microsoft.Extensions.ObjectPool" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||
<Reference Include="System.IO.Pipelines" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements PipeWriter using a underlying stream.
|
||||
/// </summary>
|
||||
public class StreamPipeWriter : PipeWriter, IDisposable
|
||||
{
|
||||
private readonly int _minimumSegmentSize;
|
||||
private readonly Stream _writingStream;
|
||||
private int _bytesWritten;
|
||||
|
||||
private List<CompletedBuffer> _completedSegments;
|
||||
private Memory<byte> _currentSegment;
|
||||
private IMemoryOwner<byte> _currentSegmentOwner;
|
||||
private MemoryPool<byte> _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new StreamPipeWrapper
|
||||
/// </summary>
|
||||
/// <param name="writingStream">The stream to write to</param>
|
||||
public StreamPipeWriter(Stream writingStream) : this(writingStream, 4096)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool<byte> pool = null)
|
||||
{
|
||||
_minimumSegmentSize = minimumSegmentSize;
|
||||
_writingStream = writingStream;
|
||||
_pool = pool ?? MemoryPool<byte>.Shared;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Memory<byte> GetMemory(int sizeHint = 0)
|
||||
{
|
||||
EnsureCapacity(sizeHint);
|
||||
|
||||
return _currentSegment;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Span<byte> GetSpan(int sizeHint = 0)
|
||||
{
|
||||
EnsureCapacity(sizeHint);
|
||||
|
||||
return _currentSegment.Span.Slice(_position);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void CancelPendingFlush()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnReaderCompleted(Action<Exception, object> callback, object state)
|
||||
{
|
||||
throw new NotSupportedException("OnReaderCompleted isn't supported in StreamPipeWrapper.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_bytesWritten == 0)
|
||||
{
|
||||
return new ValueTask<FlushResult>(new FlushResult(isCanceled: false, IsCompletedOrThrow()));
|
||||
}
|
||||
|
||||
return FlushAsyncInternal(cancellationToken);
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
InternalTokenSource.Cancel();
|
||||
}
|
||||
|
||||
private async ValueTask<FlushResult> 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<byte>(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<byte>(_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<CompletedBuffer>();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private readonly struct CompletedBuffer
|
||||
{
|
||||
public Memory<byte> Buffer { get; }
|
||||
public int Length { get; }
|
||||
|
||||
public ReadOnlySpan<byte> Span => Buffer.Span;
|
||||
|
||||
private readonly IMemoryOwner<byte> _memoryOwner;
|
||||
|
||||
public CompletedBuffer(IMemoryOwner<byte> buffer, int length)
|
||||
{
|
||||
Buffer = buffer.Memory;
|
||||
Length = length;
|
||||
_memoryOwner = buffer;
|
||||
}
|
||||
|
||||
public void Return()
|
||||
{
|
||||
_memoryOwner.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FlushResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<object>(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<object>(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<object>(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<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<IRouteValuesFeature>();
|
||||
// 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<IRouteValuesFeature>();
|
||||
// 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<IRouteValuesFeature>(customRouteValuesFeature);
|
||||
// Can override DefaultHttpRequest.RouteValues with custom feature
|
||||
Assert.Equal(customRouteValuesFeature.RouteValues, request.RouteValues);
|
||||
|
||||
// Can clear feature
|
||||
context.Features.Set<IRouteValuesFeature>(null);
|
||||
Assert.Empty(request.RouteValues);
|
||||
}
|
||||
|
||||
private class CustomRouteValuesFeature : IRouteValuesFeature
|
||||
{
|
||||
public RouteValueDictionary RouteValues { get; set; }
|
||||
}
|
||||
|
||||
private static HttpRequest CreateRequest(IHeaderDictionary headers)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte>(array, 0, 0));
|
||||
Writer.Write(new Span<byte>(array, array.Length, 0));
|
||||
|
||||
try
|
||||
{
|
||||
Writer.Write(new Span<byte>(array, offset, length));
|
||||
Assert.True(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.True(ex is ArgumentOutOfRangeException);
|
||||
}
|
||||
|
||||
Writer.Write(new Span<byte>(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<byte>(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<byte> memory = Writer.GetMemory();
|
||||
|
||||
IEnumerable<byte> 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<byte> 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<byte> buffer = Writer.GetMemory(1);
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => buffer.Advance(1));
|
||||
Assert.Equal("No writing operation. Make sure GetMemory() was called.", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvalidOperationException>(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<TaskCanceledException>(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<int>(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<FlushResult> 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<int>(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<int>(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<int>(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<FlushResult> 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<int> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte>
|
||||
{
|
||||
private MemoryPool<byte> _pool = Shared;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public override IMemoryOwner<byte> 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<byte>
|
||||
{
|
||||
private IMemoryOwner<byte> _owner;
|
||||
|
||||
private readonly TestMemoryPool _pool;
|
||||
|
||||
private int _referenceCount;
|
||||
|
||||
private bool _returned;
|
||||
|
||||
private string _leaser;
|
||||
|
||||
public PooledMemory(IMemoryOwner<byte> 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<byte> 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<byte>(((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<byte> segment)
|
||||
{
|
||||
_pool.CheckDisposed();
|
||||
return MemoryMarshal.TryGetArray(_owner.Memory, out segment);
|
||||
}
|
||||
|
||||
public override Memory<byte> Memory
|
||||
{
|
||||
get
|
||||
{
|
||||
_pool.CheckDisposed();
|
||||
return _owner.Memory;
|
||||
}
|
||||
}
|
||||
|
||||
public override Span<byte> GetSpan()
|
||||
{
|
||||
_pool.CheckDisposed();
|
||||
return _owner.Memory.Span;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue