diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs
index 271329209a..ebb3995472 100644
--- a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs
+++ b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs
@@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Authentication
{
Items[key] = value;
}
- else if (Items.ContainsKey(key))
+ else
{
Items.Remove(key);
}
@@ -169,9 +169,9 @@ namespace Microsoft.AspNetCore.Authentication
{
if (value.HasValue)
{
- Items[key] = value.Value.ToString();
+ Items[key] = value.GetValueOrDefault().ToString();
}
- else if (Items.ContainsKey(key))
+ else
{
Items.Remove(key);
}
@@ -201,9 +201,9 @@ namespace Microsoft.AspNetCore.Authentication
{
if (value.HasValue)
{
- Items[key] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture);
+ Items[key] = value.GetValueOrDefault().ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture);
}
- else if (Items.ContainsKey(key))
+ else
{
Items.Remove(key);
}
diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs
index 555da9e098..e188e98823 100644
--- a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs
+++ b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs
@@ -5,7 +5,7 @@
namespace Microsoft.AspNetCore.Authentication
{
///
- /// Name/Value representing an token.
+ /// Name/Value representing a token.
///
public class AuthenticationToken
{
@@ -19,4 +19,4 @@ namespace Microsoft.AspNetCore.Authentication
///
public string Value { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs
index 639c9b558e..84db381ce4 100644
--- a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs
+++ b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using Xunit;
@@ -73,6 +74,10 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
props.SetString("foo", null);
Assert.Null(props.GetString("foo"));
Assert.Equal(1, props.Items.Count);
+
+ props.SetString("doesntexist", null);
+ Assert.False(props.Items.ContainsKey("doesntexist"));
+ Assert.Equal(1, props.Items.Count);
}
[Fact]
@@ -203,5 +208,94 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test
props.Items.Clear();
Assert.Null(props.AllowRefresh);
}
+
+ [Fact]
+ public void SetDateTimeOffset()
+ {
+ var props = new MyAuthenticationProperties();
+
+ props.SetDateTimeOffset("foo", new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)));
+ Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items["foo"]);
+
+ props.SetDateTimeOffset("foo", null);
+ Assert.False(props.Items.ContainsKey("foo"));
+
+ props.SetDateTimeOffset("doesnotexist", null);
+ Assert.False(props.Items.ContainsKey("doesnotexist"));
+ }
+
+ [Fact]
+ public void GetDateTimeOffset()
+ {
+ var props = new MyAuthenticationProperties();
+ var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc));
+
+ props.Items["foo"] = dateTimeOffset.ToString("r", CultureInfo.InvariantCulture);
+ Assert.Equal(dateTimeOffset, props.GetDateTimeOffset("foo"));
+
+ props.Items.Remove("foo");
+ Assert.Null(props.GetDateTimeOffset("foo"));
+
+ props.Items["foo"] = "BAR";
+ Assert.Null(props.GetDateTimeOffset("foo"));
+ Assert.Equal("BAR", props.Items["foo"]);
+ }
+
+ [Fact]
+ public void SetBool()
+ {
+ var props = new MyAuthenticationProperties();
+
+ props.SetBool("foo", true);
+ Assert.Equal(true.ToString(), props.Items["foo"]);
+
+ props.SetBool("foo", false);
+ Assert.Equal(false.ToString(), props.Items["foo"]);
+
+ props.SetBool("foo", null);
+ Assert.False(props.Items.ContainsKey("foo"));
+ }
+
+ [Fact]
+ public void GetBool()
+ {
+ var props = new MyAuthenticationProperties();
+
+ props.Items["foo"] = true.ToString();
+ Assert.True(props.GetBool("foo"));
+
+ props.Items["foo"] = false.ToString();
+ Assert.False(props.GetBool("foo"));
+
+ props.Items["foo"] = null;
+ Assert.Null(props.GetBool("foo"));
+
+ props.Items["foo"] = "BAR";
+ Assert.Null(props.GetBool("foo"));
+ Assert.Equal("BAR", props.Items["foo"]);
+ }
+
+ public class MyAuthenticationProperties : AuthenticationProperties
+ {
+ public new DateTimeOffset? GetDateTimeOffset(string key)
+ {
+ return base.GetDateTimeOffset(key);
+ }
+
+ public new void SetDateTimeOffset(string key, DateTimeOffset? value)
+ {
+ base.SetDateTimeOffset(key, value);
+ }
+
+ public new void SetBool(string key, bool? value)
+ {
+ base.SetBool(key, value);
+ }
+
+ public new bool? GetBool(string key)
+ {
+ return base.GetBool(key);
+ }
+ }
}
}
diff --git a/src/Http/Headers/src/CacheControlHeaderValue.cs b/src/Http/Headers/src/CacheControlHeaderValue.cs
index 81e18faf47..655d9447ce 100644
--- a/src/Http/Headers/src/CacheControlHeaderValue.cs
+++ b/src/Http/Headers/src/CacheControlHeaderValue.cs
@@ -197,14 +197,14 @@ namespace Microsoft.Net.Http.Headers
{
AppendValueWithSeparatorIfRequired(sb, MaxAgeString);
sb.Append('=');
- sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_sharedMaxAge.HasValue)
{
AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString);
sb.Append('=');
- sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_maxStale)
@@ -213,7 +213,7 @@ namespace Microsoft.Net.Http.Headers
if (_maxStaleLimit.HasValue)
{
sb.Append('=');
- sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
}
@@ -221,7 +221,7 @@ namespace Microsoft.Net.Http.Headers
{
AppendValueWithSeparatorIfRequired(sb, MinFreshString);
sb.Append('=');
- sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_private)
@@ -291,10 +291,10 @@ namespace Microsoft.Net.Http.Headers
// XOR the hashcode of timespan values with different numbers to make sure two instances with the same
// timespan set on different fields result in different hashcodes.
- result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^
- (_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^
- (_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^
- (_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0);
+ result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^
+ (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^
+ (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^
+ (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0);
if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
{
diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs
index 392a441733..5084f8ea06 100644
--- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs
+++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs
@@ -137,11 +137,11 @@ namespace Microsoft.Net.Http.Headers
}
else if (sizeParameter != null)
{
- sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
+ sizeParameter.Value = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture);
}
else
{
- string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
+ string sizeString = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture);
_parameters.Add(new NameValueHeaderValue(SizeString, sizeString));
}
}
@@ -324,7 +324,7 @@ namespace Microsoft.Net.Http.Headers
else
{
// Must always be quoted
- var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true);
+ var dateString = HeaderUtilities.FormatDate(date.GetValueOrDefault(), quoted: true);
if (dateParameter != null)
{
dateParameter.Value = dateString;
diff --git a/src/Http/Headers/src/ContentRangeHeaderValue.cs b/src/Http/Headers/src/ContentRangeHeaderValue.cs
index 99583cdf47..1149c33865 100644
--- a/src/Http/Headers/src/ContentRangeHeaderValue.cs
+++ b/src/Http/Headers/src/ContentRangeHeaderValue.cs
@@ -151,9 +151,9 @@ namespace Microsoft.Net.Http.Headers
if (HasRange)
{
- sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(_from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo));
sb.Append('-');
- sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(_to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo));
}
else
{
@@ -163,7 +163,7 @@ namespace Microsoft.Net.Http.Headers
sb.Append('/');
if (HasLength)
{
- sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo));
+ sb.Append(_length.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo));
}
else
{
diff --git a/src/Http/Headers/src/RangeConditionHeaderValue.cs b/src/Http/Headers/src/RangeConditionHeaderValue.cs
index f1ebee276c..b1d6513e2e 100644
--- a/src/Http/Headers/src/RangeConditionHeaderValue.cs
+++ b/src/Http/Headers/src/RangeConditionHeaderValue.cs
@@ -54,7 +54,7 @@ namespace Microsoft.Net.Http.Headers
{
if (_entityTag == null)
{
- return HeaderUtilities.FormatDate(_lastModified.Value);
+ return HeaderUtilities.FormatDate(_lastModified.GetValueOrDefault());
}
return _entityTag.ToString();
}
@@ -70,7 +70,7 @@ namespace Microsoft.Net.Http.Headers
if (_entityTag == null)
{
- return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value);
+ return (other._lastModified != null) && (_lastModified.GetValueOrDefault() == other._lastModified.GetValueOrDefault());
}
return _entityTag.Equals(other._entityTag);
@@ -80,7 +80,7 @@ namespace Microsoft.Net.Http.Headers
{
if (_entityTag == null)
{
- return _lastModified.Value.GetHashCode();
+ return _lastModified.GetValueOrDefault().GetHashCode();
}
return _entityTag.GetHashCode();
diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs
index 3b177f6e9a..010f2da1e0 100644
--- a/src/Http/Headers/src/RangeItemHeaderValue.cs
+++ b/src/Http/Headers/src/RangeItemHeaderValue.cs
@@ -20,15 +20,15 @@ namespace Microsoft.Net.Http.Headers
{
throw new ArgumentException("Invalid header range.");
}
- if (from.HasValue && (from.Value < 0))
+ if (from.HasValue && (from.GetValueOrDefault() < 0))
{
throw new ArgumentOutOfRangeException(nameof(from));
}
- if (to.HasValue && (to.Value < 0))
+ if (to.HasValue && (to.GetValueOrDefault() < 0))
{
throw new ArgumentOutOfRangeException(nameof(to));
}
- if (from.HasValue && to.HasValue && (from.Value > to.Value))
+ if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault()))
{
throw new ArgumentOutOfRangeException(nameof(from));
}
@@ -51,38 +51,37 @@ namespace Microsoft.Net.Http.Headers
{
if (!_from.HasValue)
{
- return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo);
+ return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo);
}
else if (!_to.HasValue)
{
- return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-";
+ return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-";
}
- return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" +
- _to.Value.ToString(NumberFormatInfo.InvariantInfo);
+ return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" +
+ _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo);
}
public override bool Equals(object obj)
{
- var other = obj as RangeItemHeaderValue;
-
- if (other == null)
+ if (obj is RangeItemHeaderValue other)
{
- return false;
+ return ((_from == other._from) && (_to == other._to));
}
- return ((_from == other._from) && (_to == other._to));
+
+ return false;
}
public override int GetHashCode()
{
if (!_from.HasValue)
{
- return _to.GetHashCode();
+ return _to.GetValueOrDefault().GetHashCode();
}
else if (!_to.HasValue)
{
- return _from.GetHashCode();
+ return _from.GetValueOrDefault().GetHashCode();
}
- return _from.GetHashCode() ^ _to.GetHashCode();
+ return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode();
}
// Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty
diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs
index f3477648de..3070a6e1f9 100644
--- a/src/Http/Headers/src/SetCookieHeaderValue.cs
+++ b/src/Http/Headers/src/SetCookieHeaderValue.cs
@@ -105,13 +105,13 @@ namespace Microsoft.Net.Http.Headers
if (Expires.HasValue)
{
- expires = HeaderUtilities.FormatDate(Expires.Value);
+ expires = HeaderUtilities.FormatDate(Expires.GetValueOrDefault());
length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length;
}
if (MaxAge.HasValue)
{
- maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds);
+ maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds);
length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length;
}
@@ -212,12 +212,12 @@ namespace Microsoft.Net.Http.Headers
if (Expires.HasValue)
{
- AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
+ AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.GetValueOrDefault()));
}
if (MaxAge.HasValue)
{
- AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds));
+ AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds));
}
if (Domain != null)
@@ -452,9 +452,15 @@ namespace Microsoft.Net.Http.Headers
result.HttpOnly = true;
}
// extension-av =
- else
- {
- // TODO: skip it? Store it in a list?
+ else
+ {
+ // TODO: skiping it for now to avoid parsing failure? Store it in a list?
+ // = (no spaces)
+ if (!ReadEqualsSign(input, ref offset))
+ {
+ return 0;
+ }
+ ReadToSemicolonOrEnd(input, ref offset);
}
}
@@ -520,4 +526,4 @@ namespace Microsoft.Net.Http.Headers
^ HttpOnly.GetHashCode();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Http/Headers/src/StringWithQualityHeaderValue.cs b/src/Http/Headers/src/StringWithQualityHeaderValue.cs
index deba2d2697..153643b361 100644
--- a/src/Http/Headers/src/StringWithQualityHeaderValue.cs
+++ b/src/Http/Headers/src/StringWithQualityHeaderValue.cs
@@ -58,7 +58,7 @@ namespace Microsoft.Net.Http.Headers
{
if (_quality.HasValue)
{
- return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo);
+ return _value + "; q=" + _quality.GetValueOrDefault().ToString("0.0##", NumberFormatInfo.InvariantInfo);
}
return _value.ToString();
@@ -83,7 +83,7 @@ namespace Microsoft.Net.Http.Headers
// Note that we don't consider double.Epsilon here. We really consider two values equal if they're
// actually equal. This makes sure that we also get the same hashcode for two values considered equal
// by Equals().
- return other._quality.HasValue && (_quality.Value == other._quality.Value);
+ return other._quality.HasValue && (_quality.GetValueOrDefault() == other._quality.Value);
}
// If we don't have a quality value, then 'other' must also have no quality assigned in order to be
@@ -97,7 +97,7 @@ namespace Microsoft.Net.Http.Headers
if (_quality.HasValue)
{
- result = result ^ _quality.Value.GetHashCode();
+ result = result ^ _quality.GetValueOrDefault().GetHashCode();
}
return result;
diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs
index 9a920f40d0..058f8d4bd9 100644
--- a/src/Http/Headers/test/SetCookieHeaderValueTest.cs
+++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs
@@ -365,6 +365,18 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(cookies, results);
}
+ [Fact]
+ public void SetCookieHeaderValue_TryParse_SkipExtensionValues()
+ {
+ string cookieHeaderValue = "cookiename=value; extensionname=value;";
+
+ SetCookieHeaderValue setCookieHeaderValue;
+
+ SetCookieHeaderValue.TryParse(cookieHeaderValue, out setCookieHeaderValue);
+
+ Assert.Equal("value", setCookieHeaderValue.Value);
+ }
+
[Theory]
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input)
diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs
index ce89e5b054..5c0db2a46f 100644
--- a/src/Http/Http.Abstractions/src/CookieBuilder.cs
+++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs
@@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Http
Domain = Domain,
IsEssential = IsEssential,
Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps),
- Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.Value) : default(DateTimeOffset?)
+ Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.GetValueOrDefault()) : default(DateTimeOffset?)
};
}
}
diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs
index a4337b7766..4c4d0d1af1 100644
--- a/src/Http/Http.Abstractions/src/HttpRequest.cs
+++ b/src/Http/Http.Abstractions/src/HttpRequest.cs
@@ -4,6 +4,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Http
{
@@ -66,9 +67,9 @@ namespace Microsoft.AspNetCore.Http
public abstract IQueryCollection Query { get; set; }
///
- /// Gets or sets the RequestProtocol.
+ /// Gets or sets the request protocol (e.g. HTTP/1.1).
///
- /// The RequestProtocol.
+ /// The request protocol.
public abstract string Protocol { get; set; }
///
@@ -117,5 +118,11 @@ namespace Microsoft.AspNetCore.Http
///
///
public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
+
+ ///
+ /// Gets the collection of route values for this request.
+ ///
+ /// The collection of route values for this request.
+ public virtual RouteValueDictionary RouteValues { get; set; }
}
}
diff --git a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs
index 185fc40ac7..971447fa44 100644
--- a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs
+++ b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs
@@ -103,13 +103,13 @@ namespace Microsoft.AspNetCore.Http.Internal
{
throw new ArgumentNullException(nameof(key));
}
- if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value))
+ if (!values.HasValue || StringValues.IsNullOrEmpty(values.GetValueOrDefault()))
{
headers.Remove(key);
}
else
{
- headers[key] = values.Value;
+ headers[key] = values.GetValueOrDefault();
}
}
diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj
index 821b40cb19..b95ea9c28c 100644
--- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj
+++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj
@@ -17,6 +17,7 @@ Microsoft.AspNetCore.Http.HttpResponse
+
diff --git a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs
index 6af7d138be..02985d630b 100644
--- a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs
+++ b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs
@@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions
internal static string FormatArgumentCannotBeNullOrEmpty()
=> GetString("ArgumentCannotBeNullOrEmpty");
+ ///
+ /// An element with the key '{0}' already exists in the {1}.
+ ///
+ internal static string RouteValueDictionary_DuplicateKey
+ {
+ get => GetString("RouteValueDictionary_DuplicateKey");
+ }
+
+ ///
+ /// An element with the key '{0}' already exists in the {1}.
+ ///
+ internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
+
+ ///
+ /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
+ ///
+ internal static string RouteValueDictionary_DuplicatePropertyName
+ {
+ get => GetString("RouteValueDictionary_DuplicatePropertyName");
+ }
+
+ ///
+ /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
+ ///
+ internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Http/Http.Abstractions/src/Resources.resx b/src/Http/Http.Abstractions/src/Resources.resx
index dfdfeaf7d1..4ce2d8bd7a 100644
--- a/src/Http/Http.Abstractions/src/Resources.resx
+++ b/src/Http/Http.Abstractions/src/Resources.resx
@@ -156,4 +156,10 @@
Argument cannot be null or empty.
+
+ An element with the key '{0}' already exists in the {1}.
+
+
+ The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
+
\ No newline at end of file
diff --git a/src/Http/Http.Abstractions/src/Routing/Endpoint.cs b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs
new file mode 100644
index 0000000000..16202b45f5
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs
@@ -0,0 +1,49 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http
+{
+ ///
+ /// Respresents a logical endpoint in an application.
+ ///
+ public class Endpoint
+ {
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The delegate used to process requests for the endpoint.
+ ///
+ /// The endpoint . May be null.
+ ///
+ ///
+ /// The informational display name of the endpoint. May be null.
+ ///
+ public Endpoint(
+ RequestDelegate requestDelegate,
+ EndpointMetadataCollection metadata,
+ string displayName)
+ {
+ // All are allowed to be null
+ RequestDelegate = requestDelegate;
+ Metadata = metadata ?? EndpointMetadataCollection.Empty;
+ DisplayName = displayName;
+ }
+
+ ///
+ /// Gets the informational display name of this endpoint.
+ ///
+ public string DisplayName { get; }
+
+ ///
+ /// Gets the collection of metadata associated with this endpoint.
+ ///
+ public EndpointMetadataCollection Metadata { get; }
+
+ ///
+ /// Gets the delegate used to process requests for the endpoint.
+ ///
+ public RequestDelegate RequestDelegate { get; }
+
+ public override string ToString() => DisplayName ?? base.ToString();
+ }
+}
diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs
new file mode 100644
index 0000000000..642af5f4d5
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Http.Endpoints
+{
+ ///
+ /// Extension methods to expose Endpoint on HttpContext.
+ ///
+ public static class EndpointHttpContextExtensions
+ {
+ ///
+ /// Extension method for getting the for the current request.
+ ///
+ /// The context.
+ /// The .
+ public static Endpoint GetEndpoint(this HttpContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return context.Features.Get()?.Endpoint;
+ }
+
+ ///
+ /// Extension method for setting the for the current request.
+ ///
+ /// The context.
+ /// The .
+ public static void SetEndpoint(this HttpContext context, Endpoint endpoint)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var feature = context.Features.Get();
+
+ if (endpoint != null)
+ {
+ if (feature == null)
+ {
+ feature = new EndpointFeature();
+ context.Features.Set(feature);
+ }
+
+ feature.Endpoint = endpoint;
+ }
+ else
+ {
+ if (feature == null)
+ {
+ // No endpoint to set and no feature on context. Do nothing
+ return;
+ }
+
+ feature.Endpoint = null;
+ }
+ }
+
+ private class EndpointFeature : IEndpointFeature
+ {
+ public Endpoint Endpoint { get; set; }
+ }
+ }
+}
diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs
new file mode 100644
index 0000000000..a792fff295
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs
@@ -0,0 +1,201 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.AspNetCore.Http
+{
+ ///
+ /// A collection of arbitrary metadata associated with an endpoint.
+ ///
+ ///
+ /// instances contain a list of metadata items
+ /// of arbitrary types. The metadata items are stored as an ordered collection with
+ /// items arranged in ascending order of precedence.
+ ///
+ public sealed class EndpointMetadataCollection : IReadOnlyList