Merge the master branch of aspnet/HttpAbstractions

This commit is contained in:
Nate McMaster 2018-11-20 09:49:01 -08:00
commit ee1e2d6173
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
59 changed files with 5558 additions and 101 deletions

View File

@ -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);
}

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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))
{

View File

@ -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;

View File

@ -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
{

View File

@ -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();

View File

@ -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

View File

@ -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();
}
}
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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?)
};
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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; }
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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

View File

@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http
if (value.HasValue)
{
headers[name] = HeaderUtilities.FormatDate(value.Value);
headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault());
}
else
{

View File

@ -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);
}

View File

@ -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);

View File

@ -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)

View File

@ -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]

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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
{

View File

@ -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;
}
}
}

View File

@ -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
{

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
};

View File

@ -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;
}
}
}

View File

@ -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>();

View File

@ -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>

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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]

View File

@ -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()
{

View File

@ -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();

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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
{

View File

@ -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;

View File

@ -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);