diff --git a/src/Microsoft.AspNet.Http.Abstractions/FragmentString.cs b/src/Microsoft.AspNet.Http.Abstractions/FragmentString.cs index 87b22df2..41757d29 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/FragmentString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/FragmentString.cs @@ -100,7 +100,7 @@ public static FragmentString FromUriComponent(Uri uri) string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); if (!string.IsNullOrEmpty(fragmentValue)) { - fragmentValue = "#" + fragmentValue; + fragmentValue = $"#{fragmentValue}"; } return new FragmentString(fragmentValue); } diff --git a/src/Microsoft.AspNet.Http.Abstractions/HostString.cs b/src/Microsoft.AspNet.Http.Abstractions/HostString.cs index 59fb4f23..b5debac3 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/HostString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/HostString.cs @@ -70,18 +70,18 @@ public string ToUriComponent() && _value.IndexOf(':', index + 1) >= 0) { // IPv6 without brackets ::1 is the only type of host with 2 or more colons - return "[" + _value + "]"; + return $"[{_value}]"; } else if (index >= 0) { // Has a port string port = _value.Substring(index); - IdnMapping mapping = new IdnMapping(); + var mapping = new IdnMapping(); return mapping.GetAscii(_value, 0, index) + port; } else { - IdnMapping mapping = new IdnMapping(); + var mapping = new IdnMapping(); return mapping.GetAscii(_value); } } @@ -115,12 +115,12 @@ public static HostString FromUriComponent(string uriComponent) { // Has a port string port = uriComponent.Substring(index); - IdnMapping mapping = new IdnMapping(); + var mapping = new IdnMapping(); uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; } else { - IdnMapping mapping = new IdnMapping(); + var mapping = new IdnMapping(); uriComponent = mapping.GetUnicode(uriComponent); } } diff --git a/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs b/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs index 283eddf4..c0f52fd4 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs @@ -24,13 +24,13 @@ public abstract class HttpRequest public abstract string Method { get; set; } /// - /// Gets or set the HTTP request scheme from owin.RequestScheme. + /// Gets or set the HTTP request scheme. /// - /// The HTTP request scheme from owin.RequestScheme. + /// The HTTP request scheme. public abstract string Scheme { get; set; } /// - /// Returns true if the owin.RequestScheme is https. + /// Returns true if the RequestScheme is https. /// /// true if this request is using https; otherwise, false. public abstract bool IsHttps { get; set; } @@ -42,33 +42,33 @@ public abstract class HttpRequest public abstract HostString Host { get; set; } /// - /// Gets or set the owin.RequestPathBase. + /// Gets or set the RequestPathBase. /// - /// The owin.RequestPathBase. + /// The RequestPathBase. public abstract PathString PathBase { get; set; } /// - /// Gets or set the request path from owin.RequestPath. + /// Gets or set the request path from RequestPath. /// - /// The request path from owin.RequestPath. + /// The request path from RequestPath. public abstract PathString Path { get; set; } /// - /// Gets or set the query string from owin.RequestQueryString. + /// Gets or set the query string. /// - /// The query string from owin.RequestQueryString. + /// The query string. public abstract QueryString QueryString { get; set; } /// - /// Gets the query value collection parsed from owin.RequestQueryString. + /// Gets the query value collection parsed from RequestQueryString. /// - /// The query value collection parsed from owin.RequestQueryString. - public abstract IReadableStringCollection Query { get; set; } + /// The query value collection parsed from RequestQueryString. + public abstract IQueryCollection Query { get; set; } /// - /// Gets or set the owin.RequestProtocol. + /// Gets or set the RequestProtocol. /// - /// The owin.RequestProtocol. + /// The RequestProtocol. public abstract string Protocol { get; set; } /// @@ -81,7 +81,7 @@ public abstract class HttpRequest /// Gets the collection of Cookies for this request. /// /// The collection of Cookies for this request. - public abstract IReadableStringCollection Cookies { get; set; } + public abstract IRequestCookieCollection Cookies { get; set; } /// /// Gets or sets the Content-Length header @@ -95,9 +95,9 @@ public abstract class HttpRequest public abstract string ContentType { get; set; } /// - /// Gets or set the owin.RequestBody Stream. + /// Gets or set the RequestBody Stream. /// - /// The owin.RequestBody Stream. + /// The RequestBody Stream. public abstract Stream Body { get; set; } /// diff --git a/src/Microsoft.AspNet.Http.Abstractions/IFormCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IFormCollection.cs index 68505962..03e81168 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IFormCollection.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IFormCollection.cs @@ -1,13 +1,95 @@ // 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.Collections.Generic; +using Microsoft.Extensions.Primitives; + namespace Microsoft.AspNet.Http { /// - /// Contains the parsed form values. + /// Represents the parsed form values sent with the HttpRequest. /// - public interface IFormCollection : IReadableStringCollection + public interface IFormCollection : IEnumerable> { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or .Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } + + /// + /// The file collection sent with the request. + /// + /// + /// The files included with the request. IFormFileCollection Files { get; } } } diff --git a/src/Microsoft.AspNet.Http.Abstractions/IFormFile.cs b/src/Microsoft.AspNet.Http.Abstractions/IFormFile.cs index e85ee75f..6f8fdaa2 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IFormFile.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IFormFile.cs @@ -5,6 +5,9 @@ namespace Microsoft.AspNet.Http { + /// + /// Represents a file sent with the HttpRequest. + /// public interface IFormFile { string ContentType { get; } diff --git a/src/Microsoft.AspNet.Http.Abstractions/IFormFileCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IFormFileCollection.cs index 4950758b..229b7bbb 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IFormFileCollection.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IFormFileCollection.cs @@ -5,6 +5,9 @@ namespace Microsoft.AspNet.Http { + /// + /// Represents the collection of files sent with the HttpRequest. + /// public interface IFormFileCollection : IReadOnlyList { IFormFile this[string name] { get; } diff --git a/src/Microsoft.AspNet.Http.Abstractions/IQueryCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IQueryCollection.cs new file mode 100644 index 00000000..4bed287e --- /dev/null +++ b/src/Microsoft.AspNet.Http.Abstractions/IQueryCollection.cs @@ -0,0 +1,88 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNet.Http +{ + /// + /// Represents the HttpRequest query string collection + /// + public interface IQueryCollection : IEnumerable> + { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or .Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } + } +} diff --git a/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs deleted file mode 100644 index 81c84087..00000000 --- a/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNet.Http -{ - /// - /// Accessors for headers, query, forms, etc. - /// - public interface IReadableStringCollection : IEnumerable> - { - /// - /// Get the associated value from the collection. - /// Returns StringValues.Empty if the key is not present. - /// - /// - /// - StringValues this[string key] { get; } - - /// - /// Gets the number of elements contained in the collection. - /// - int Count { get; } - - /// - /// Gets a collection containing the keys. - /// - ICollection Keys { get; } - - /// - /// Determines whether the collection contains an element with the specified key. - /// - /// - /// - bool ContainsKey(string key); - } -} diff --git a/src/Microsoft.AspNet.Http.Abstractions/IRequestCookieCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IRequestCookieCollection.cs new file mode 100644 index 00000000..5cc028b1 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Abstractions/IRequestCookieCollection.cs @@ -0,0 +1,87 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNet.Http +{ + /// + /// Represents the HttpRequest cookie collection + /// + public interface IRequestCookieCollection : IEnumerable> + { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out string value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or .Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return String.Empty for missing entries + /// rather than throwing an Exception. + /// + string this[string key] { get; } + } +} diff --git a/src/Microsoft.AspNet.Http.Abstractions/PathString.cs b/src/Microsoft.AspNet.Http.Abstractions/PathString.cs index ebcc9b10..8a76d8f5 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/PathString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/PathString.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Http /// public struct PathString : IEquatable { + private static readonly char[] splitChar = { '/' }; + /// /// Represents the empty path. This field is read-only. /// @@ -66,7 +68,24 @@ public override string ToString() public string ToUriComponent() { // TODO: Measure the cost of this escaping and consider optimizing. - return HasValue ? string.Join("/", _value.Split('/').Select(UrlEncoder.Default.Encode)) : string.Empty; + if (!HasValue) + { + return string.Empty; + } + var values = _value.Split(splitChar); + var changed = false; + for (var i = 0; i < values.Length; i++) + { + var value = values[i]; + values[i] = UrlEncoder.Default.Encode(value); + + if (!changed && value != values[i]) + { + changed = true; + } + } + + return changed ? string.Join("/", values) : _value; } /// diff --git a/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs index af2feeed..088402b2 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs @@ -128,7 +128,7 @@ public static QueryString Create(string name, string value) throw new ArgumentNullException(nameof(value)); } - return new QueryString("?" + UrlEncoder.Default.Encode(name) + '=' + UrlEncoder.Default.Encode(value)); + return new QueryString($"?{UrlEncoder.Default.Encode(name)}={UrlEncoder.Default.Encode(value)}"); } /// diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs index 6a8d909d..fedf74ab 100644 --- a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs @@ -36,7 +36,7 @@ public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, st /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) { - return ParsingHelpers.GetHeaderSplit(headers, key); + return ParsingHelpers.GetHeaderSplit(headers, key).ToArray(); } /// diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs index 5b1f70f4..c9ac784d 100644 --- a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs @@ -78,9 +78,49 @@ internal static void SetList(this IHeaderDictionary headers, string name, ILi { headers.Remove(name); } + else if (values.Count == 1) + { + headers[name] = new StringValues(values[0].ToString()); + } else { - headers[name] = values.Select(value => value.ToString()).ToArray(); + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + headers[name] = new StringValues(newValues); + } + } + + public static void AppendList(this IHeaderDictionary Headers, string name, IList values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + switch (values.Count) + { + case 0: + Headers.Append(name, StringValues.Empty); + break; + case 1: + Headers.Append(name, new StringValues(values[0].ToString())); + break; + default: + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + Headers.Append(name, new StringValues(newValues)); + break; } } @@ -139,7 +179,7 @@ internal static T Get(this IHeaderDictionary headers, string name) if (KnownParsers.TryGetValue(typeof(T), out temp)) { var func = (Func)temp; - return func(headers[name]); + return func(headers[name].ToString()); } var value = headers[name]; @@ -148,7 +188,7 @@ internal static T Get(this IHeaderDictionary headers, string name) return default(T); } - return GetViaReflection(value); + return GetViaReflection(value.ToString()); } internal static IList GetList(this IHeaderDictionary headers, string name) @@ -162,7 +202,7 @@ internal static IList GetList(this IHeaderDictionary headers, string name) if (KnownListParsers.TryGetValue(typeof(T), out temp)) { var func = (Func, IList>)temp; - return func(headers[name]); + return func(headers[name].ToArray()); } var values = headers[name]; @@ -179,7 +219,7 @@ private static T GetViaReflection(string value) // TODO: Cache the reflected type for later? Only if success? var type = typeof(T); var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(methodInfo => + .FirstOrDefault(methodInfo => { if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal) && methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool))) @@ -191,7 +231,7 @@ private static T GetViaReflection(string value) && methodParams[1].ParameterType.Equals(type.MakeByRefType()); } return false; - }).FirstOrDefault(); + }); if (method == null) { @@ -213,7 +253,7 @@ private static IList GetListViaReflection(StringValues values) // TODO: Cache the reflected type for later? Only if success? var type = typeof(T); var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(methodInfo => + .FirstOrDefault(methodInfo => { if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal) && methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean))) @@ -225,7 +265,7 @@ private static IList GetListViaReflection(StringValues values) && methodParams[1].ParameterType.Equals(typeof(IList).MakeByRefType()); } return false; - }).FirstOrDefault(); + }); if (method == null) { diff --git a/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegment.cs b/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegment.cs index 72e94d3e..acd3a9ad 100644 --- a/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegment.cs +++ b/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegment.cs @@ -1,4 +1,9 @@ -using System; +// 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.Extensions.Primitives; + namespace Microsoft.AspNet.Http.Internal { diff --git a/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegmentCollection.cs b/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegmentCollection.cs index 6806dbaa..693bce0c 100644 --- a/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegmentCollection.cs +++ b/src/Microsoft.AspNet.Http.Extensions/Internal/HeaderSegmentCollection.cs @@ -1,3 +1,6 @@ +// 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.Generic; @@ -16,7 +19,7 @@ public HeaderSegmentCollection(StringValues headers) public bool Equals(HeaderSegmentCollection other) { - return Equals(_headers, other._headers); + return StringValues.Equals(_headers, other._headers); } public override bool Equals(object obj) diff --git a/src/Microsoft.AspNet.Http.Extensions/Internal/ParsingHelpers.cs b/src/Microsoft.AspNet.Http.Extensions/Internal/ParsingHelpers.cs index f829eec9..8d1e5f83 100644 --- a/src/Microsoft.AspNet.Http.Extensions/Internal/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.Http.Extensions/Internal/ParsingHelpers.cs @@ -10,13 +10,13 @@ namespace Microsoft.AspNet.Http.Internal { internal static class ParsingHelpers { - public static StringValues GetHeader(IDictionary headers, string key) + public static StringValues GetHeader(IHeaderDictionary headers, string key) { StringValues value; return headers.TryGetValue(key, out value) ? value : StringValues.Empty; } - public static StringValues GetHeaderSplit(IDictionary headers, string key) + public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) { var values = GetHeaderUnmodified(headers, key); return new StringValues(GetHeaderSplitImplementation(values).ToArray()); @@ -33,7 +33,7 @@ private static IEnumerable GetHeaderSplitImplementation(StringValues val } } - public static StringValues GetHeaderUnmodified(IDictionary headers, string key) + public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) { if (headers == null) { @@ -44,7 +44,7 @@ public static StringValues GetHeaderUnmodified(IDictionary return headers.TryGetValue(key, out values) ? values : StringValues.Empty; } - public static void SetHeaderJoined(IDictionary headers, string key, StringValues value) + public static void SetHeaderJoined(IHeaderDictionary headers, string key, StringValues value) { if (headers == null) { @@ -61,35 +61,26 @@ public static void SetHeaderJoined(IDictionary headers, st } else { - headers[key] = string.Join(",", value.Select(QuoteIfNeeded)); + headers[key] = string.Join(",", value.Select((s) => QuoteIfNeeded(s))); } } // Quote items that contain comas and are not already quoted. private static string QuoteIfNeeded(string value) { - if (string.IsNullOrWhiteSpace(value)) - { - // Ignore - } - else if (value.Contains(',')) - { - if (value[0] != '"' || value[value.Length - 1] != '"') - { - value = '"' + value + '"'; - } + if (!string.IsNullOrWhiteSpace(value) && + value.Contains(',') && + (value[0] != '"' || value[value.Length - 1] != '"')) + { + return $"\"{value}\""; } - return value; } private static string DeQuote(string value) { - if (string.IsNullOrWhiteSpace(value)) - { - // Ignore - } - else if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') + if (!string.IsNullOrWhiteSpace(value) && + (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) { value = value.Substring(1, value.Length - 2); } @@ -97,7 +88,7 @@ private static string DeQuote(string value) return value; } - public static void SetHeaderUnmodified(IDictionary headers, string key, StringValues? values) + public static void SetHeaderUnmodified(IHeaderDictionary headers, string key, StringValues? values) { if (headers == null) { @@ -118,7 +109,7 @@ public static void SetHeaderUnmodified(IDictionary headers } } - public static void AppendHeaderJoined(IDictionary headers, string key, params string[] values) + public static void AppendHeaderJoined(IHeaderDictionary headers, string key, params string[] values) { if (headers == null) { @@ -135,7 +126,7 @@ public static void AppendHeaderJoined(IDictionary headers, return; } - string existing = GetHeader(headers, key); + string existing = GetHeader(headers, key).ToString(); if (existing == null) { SetHeaderJoined(headers, key, values); @@ -146,7 +137,7 @@ public static void AppendHeaderJoined(IDictionary headers, } } - public static void AppendHeaderUnmodified(IDictionary headers, string key, StringValues values) + public static void AppendHeaderUnmodified(IHeaderDictionary headers, string key, StringValues values) { if (headers == null) { diff --git a/src/Microsoft.AspNet.Http.Extensions/Internal/StringSegment.cs b/src/Microsoft.AspNet.Http.Extensions/Internal/StringSegment.cs deleted file mode 100644 index 83c99842..00000000 --- a/src/Microsoft.AspNet.Http.Extensions/Internal/StringSegment.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Http.Internal -{ - internal struct StringSegment : IEquatable - { - private readonly string _buffer; - private readonly int _offset; - private readonly int _count; - - // - // Initializes a new instance of the class. - // - public StringSegment(string buffer, int offset, int count) - { - _buffer = buffer; - _offset = offset; - _count = count; - } - - public string Buffer - { - get { return _buffer; } - } - - public int Offset - { - get { return _offset; } - } - - public int Count - { - get { return _count; } - } - - public string Value - { - get { return _offset == -1 ? null : _buffer.Substring(_offset, _count); } - } - - public bool HasValue - { - get { return _offset != -1 && _count != 0 && _buffer != null; } - } - - public bool Equals(StringSegment other) - { - return string.Equals(_buffer, other._buffer) && _offset == other._offset && _count == other._count; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is StringSegment && Equals((StringSegment)obj); - } - - public override int GetHashCode() - { - unchecked - { - int hashCode = (_buffer != null ? _buffer.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ _offset; - hashCode = (hashCode * 397) ^ _count; - return hashCode; - } - } - - public static bool operator ==(StringSegment left, StringSegment right) - { - return left.Equals(right); - } - - public static bool operator !=(StringSegment left, StringSegment right) - { - return !left.Equals(right); - } - - public bool StartsWith(string text, StringComparison comparisonType) - { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - - int textLength = text.Length; - if (!HasValue || _count < textLength) - { - return false; - } - - return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; - } - - public bool EndsWith(string text, StringComparison comparisonType) - { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - - int textLength = text.Length; - if (!HasValue || _count < textLength) - { - return false; - } - - return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0; - } - - public bool Equals(string text, StringComparison comparisonType) - { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - - int textLength = text.Length; - if (!HasValue || _count != textLength) - { - return false; - } - - return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; - } - - public string Substring(int offset, int length) - { - return _buffer.Substring(_offset + offset, length); - } - - public StringSegment Subsegment(int offset, int length) - { - return new StringSegment(_buffer, _offset + offset, length); - } - - public override string ToString() - { - return Value ?? string.Empty; - } - } - -} diff --git a/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs index 4b31e306..800c0def 100644 --- a/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs +++ b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using Microsoft.AspNet.Http.Extensions; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Http.Headers @@ -170,7 +170,7 @@ public HostString Host { get { - return HostString.FromUriComponent(Headers[HeaderNames.Host]); + return HostString.FromUriComponent(Headers[HeaderNames.Host].ToString()); } set { @@ -309,17 +309,7 @@ public void Append(string name, object value) public void AppendList(string name, IList values) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - Headers.Append(name, values.Select(value => value.ToString()).ToArray()); + Headers.AppendList(name, values); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs b/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs index e7cb97ad..baad250f 100644 --- a/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs +++ b/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.AspNet.Http.Extensions; using Microsoft.Net.Http.Headers; @@ -135,7 +134,7 @@ public Uri Location get { Uri uri; - if (Uri.TryCreate(Headers[HeaderNames.Location], UriKind.RelativeOrAbsolute, out uri)) + if (Uri.TryCreate(Headers[HeaderNames.Location].ToString(), UriKind.RelativeOrAbsolute, out uri)) { return uri; } @@ -206,17 +205,7 @@ public void Append(string name, object value) public void AppendList(string name, IList values) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - Headers.Append(name, values.Select(value => value.ToString()).ToArray()); + Headers.AppendList(name, values); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs b/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs index 84143af2..e0d07b6a 100644 --- a/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs +++ b/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs @@ -25,7 +25,7 @@ public static string Encode( FragmentString fragment = new FragmentString()) { string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; - return combinePath + query + fragment; + return $"{combinePath}{query.ToString()}{fragment.ToString()}"; } /// @@ -48,7 +48,7 @@ public static string Encode( FragmentString fragment = new FragmentString()) { string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; - return scheme + "://" + host + combinePath + query + fragment; + return $"{scheme}://{host.ToString()}{combinePath}{query.ToString()}{fragment.ToString()}"; } /// diff --git a/src/Microsoft.AspNet.Http.Features/IHeaderDictionary.cs b/src/Microsoft.AspNet.Http.Features/IHeaderDictionary.cs index 0c03c29d..303ff36b 100644 --- a/src/Microsoft.AspNet.Http.Features/IHeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http.Features/IHeaderDictionary.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Http { /// - /// Represents request and response headers + /// Represents HttpRequest and HttpResponse headers /// public interface IHeaderDictionary : IDictionary { diff --git a/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs b/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs index 99f39264..c39f01cb 100644 --- a/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs @@ -140,11 +140,11 @@ public override bool IsHttps public override HostString Host { - get { return HostString.FromUriComponent(Headers["Host"]); } + get { return HostString.FromUriComponent(Headers["Host"].ToString()); } set { Headers["Host"] = value.ToUriComponent(); } } - public override IReadableStringCollection Query + public override IQueryCollection Query { get { return QueryFeature.Query; } set { QueryFeature.Query = value; } @@ -161,7 +161,7 @@ public override IHeaderDictionary Headers get { return HttpRequestFeature.Headers; } } - public override IReadableStringCollection Cookies + public override IRequestCookieCollection Cookies { get { return RequestCookiesFeature.Cookies; } set { RequestCookiesFeature.Cookies = value; } diff --git a/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs b/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs index f239261b..3eade210 100644 --- a/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs @@ -87,7 +87,7 @@ public override string ContentType { get { - return Headers[HeaderNames.ContentType]; + return Headers[HeaderNames.ContentType].ToString(); } set { diff --git a/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs b/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs index 454b0fa6..e7394a53 100644 --- a/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs +++ b/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs @@ -55,7 +55,7 @@ public override IList WebSocketRequestedProtocols { get { - return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols).ToArray(); } } diff --git a/src/Microsoft.AspNet.Http/Features/FeatureHelpers.cs b/src/Microsoft.AspNet.Http/Features/FeatureHelpers.cs index fc59b00e..ca6d914a 100644 --- a/src/Microsoft.AspNet.Http/Features/FeatureHelpers.cs +++ b/src/Microsoft.AspNet.Http/Features/FeatureHelpers.cs @@ -5,12 +5,13 @@ namespace Microsoft.AspNet.Http.Features { - internal sealed class FeatureHelpers + internal static class FeatureHelpers { public static T GetAndCache( IFeatureCache cache, IFeatureCollection features, ref T cachedObject) + where T : class { cache.CheckFeaturesRevision(); @@ -26,6 +27,7 @@ public static T GetAndCache( public static T GetOrCreate( IFeatureCollection features, Func factory) + where T : class { T obj = features.Get(); if (obj == null) @@ -43,6 +45,7 @@ public static T GetOrCreateAndCache( IFeatureCollection features, Func factory, ref T cachedObject) + where T : class { cache.CheckFeaturesRevision(); @@ -65,6 +68,7 @@ public static T GetOrCreateAndCache( IFeatureCollection features, Func factory, ref T cachedObject) + where T : class { cache.CheckFeaturesRevision(); @@ -88,6 +92,7 @@ public static T GetOrCreateAndCache( HttpRequest request, Func factory, ref T cachedObject) + where T : class { cache.CheckFeaturesRevision(); diff --git a/src/Microsoft.AspNet.Http/Features/FormFeature.cs b/src/Microsoft.AspNet.Http/Features/FormFeature.cs index b5e71b8c..f3ed68e3 100644 --- a/src/Microsoft.AspNet.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/FormFeature.cs @@ -2,14 +2,12 @@ // 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.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.WebUtilities; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Http.Features.Internal @@ -17,6 +15,8 @@ namespace Microsoft.AspNet.Http.Features.Internal public class FormFeature : IFormFeature { private readonly HttpRequest _request; + private Task _parsedFormTask; + private IFormCollection _form; public FormFeature(IFormCollection form) { @@ -63,7 +63,15 @@ public bool HasFormContentType } } - public IFormCollection Form { get; set; } + public IFormCollection Form + { + get { return _form; } + set + { + _parsedFormTask = null; + _form = value; + } + } public IFormCollection ReadForm() { @@ -77,17 +85,32 @@ public IFormCollection ReadForm() throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); } + // TODO: Issue #456 Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx // TODO: How do we prevent thread exhaustion? - return ReadFormAsync(CancellationToken.None).GetAwaiter().GetResult(); + return ReadFormAsync().GetAwaiter().GetResult(); } - public async Task ReadFormAsync(CancellationToken cancellationToken) + public Task ReadFormAsync() => ReadFormAsync(CancellationToken.None); + + public Task ReadFormAsync(CancellationToken cancellationToken) { - if (Form != null) + // Avoid state machine and task allocation for repeated reads + if (_parsedFormTask == null) { - return Form; + if (Form != null) + { + _parsedFormTask = Task.FromResult(Form); + } + else + { + _parsedFormTask = InnerReadFormAsync(cancellationToken); + } } + return _parsedFormTask; + } + private async Task InnerReadFormAsync(CancellationToken cancellationToken) + { if (!HasFormContentType) { throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); @@ -97,18 +120,18 @@ public async Task ReadFormAsync(CancellationToken cancellationT _request.EnableRewind(); - IDictionary formFields = null; - var files = new FormFileCollection(); + FormCollection formFields = null; + FormFileCollection files = null; // Some of these code paths use StreamReader which does not support cancellation tokens. - using (cancellationToken.Register(_request.HttpContext.Abort)) + using (cancellationToken.Register((state) => ((HttpContext)state).Abort(), _request.HttpContext)) { var contentType = ContentType; // Check the content-type if (HasApplicationFormContentType(contentType)) { var encoding = FilterEncoding(contentType.Encoding); - formFields = await FormReader.ReadFormAsync(_request.Body, encoding, cancellationToken); + formFields = new FormCollection(await FormReader.ReadFormAsync(_request.Body, encoding, cancellationToken)); } else if (HasMultipartFormContentType(contentType)) { @@ -119,9 +142,8 @@ public async Task ReadFormAsync(CancellationToken cancellationT var section = await multipartReader.ReadNextSectionAsync(cancellationToken); while (section != null) { - var headers = new HeaderDictionary(section.Headers); ContentDispositionHeaderValue contentDisposition; - ContentDispositionHeaderValue.TryParse(headers[HeaderNames.ContentDisposition], out contentDisposition); + ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); if (HasFileContentDisposition(contentDisposition)) { // Find the end @@ -129,8 +151,12 @@ public async Task ReadFormAsync(CancellationToken cancellationT var file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length) { - Headers = headers, + Headers = new HeaderDictionary(section.Headers), }; + if (files == null) + { + files = new FormFileCollection(); + } files.Add(file); } else if (HasFormDataContentDisposition(contentDisposition)) @@ -141,7 +167,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name); MediaTypeHeaderValue mediaType; - MediaTypeHeaderValue.TryParse(headers[HeaderNames.ContentType], out mediaType); + MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType); var encoding = FilterEncoding(mediaType?.Encoding); using (var reader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) { @@ -151,20 +177,35 @@ public async Task ReadFormAsync(CancellationToken cancellationT } else { - System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + headers[HeaderNames.ContentDisposition]); + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); } section = await multipartReader.ReadNextSectionAsync(cancellationToken); } - formFields = formAccumulator.GetResults(); + if (formAccumulator.HasValues) + { + formFields = new FormCollection(formAccumulator.GetResults(), files); + } } } // Rewind so later readers don't have to. _request.Body.Seek(0, SeekOrigin.Begin); - Form = new FormCollection(formFields, files); + if (formFields != null) + { + Form = formFields; + } + else if (files != null) + { + Form = new FormCollection(null, files); + } + else + { + Form = FormCollection.Empty; + } + return Form; } diff --git a/src/Microsoft.AspNet.Http/Features/FormFile.cs b/src/Microsoft.AspNet.Http/Features/FormFile.cs index 557dc9d5..d803cd21 100644 --- a/src/Microsoft.AspNet.Http/Features/FormFile.cs +++ b/src/Microsoft.AspNet.Http/Features/FormFile.cs @@ -21,13 +21,13 @@ public FormFile(Stream baseStream, long baseStreamOffset, long length) public string ContentDisposition { - get { return Headers["Content-Disposition"]; } + get { return Headers["Content-Disposition"].ToString(); } set { Headers["Content-Disposition"] = value; } } public string ContentType { - get { return Headers["Content-Type"]; } + get { return Headers["Content-Type"].ToString(); } set { Headers["Content-Type"] = value; } } diff --git a/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs b/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs index 1b773b93..8a8e00f2 100644 --- a/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading; -using Microsoft.AspNet.Http.Features; namespace Microsoft.AspNet.Http.Features.Internal { diff --git a/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs b/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs index a814e385..269e31ba 100644 --- a/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal { public interface IQueryFeature { - IReadableStringCollection Query { get; set; } + IQueryCollection Query { get; set; } } } diff --git a/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs index 73f23d03..b015b2d8 100644 --- a/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal { public interface IRequestCookiesFeature { - IReadableStringCollection Cookies { get; set; } + IRequestCookieCollection Cookies { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs index b2f59e00..e17e6125 100644 --- a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs @@ -2,10 +2,8 @@ // 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 Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.WebUtilities; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNet.Http.Features.Internal { @@ -17,18 +15,9 @@ public class QueryFeature : IQueryFeature, IFeatureCache private IHttpRequestFeature _request; private string _original; - private IReadableStringCollection _parsedValues; + private IQueryCollection _parsedValues; - public QueryFeature(IDictionary query) - : this(new ReadableStringCollection(query)) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - } - - public QueryFeature(IReadableStringCollection query) + public QueryFeature(IQueryCollection query) { if (query == null) { @@ -62,20 +51,34 @@ private IHttpRequestFeature HttpRequestFeature get { return FeatureHelpers.GetAndCache(this, _features, ref _request); } } - public IReadableStringCollection Query + public IQueryCollection Query { get { if (_features == null) { - return _parsedValues ?? ReadableStringCollection.Empty; + if (_parsedValues == null) + { + _parsedValues = QueryCollection.Empty; + } + return _parsedValues; } var current = HttpRequestFeature.QueryString; if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) { _original = current; - _parsedValues = new ReadableStringCollection(QueryHelpers.ParseQuery(current)); + + var result = QueryHelpers.ParseNullableQuery(current); + + if (result == null) + { + _parsedValues = QueryCollection.Empty; + } + else + { + _parsedValues = new QueryCollection(result); + } } return _parsedValues; } diff --git a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs index 9eba2dc9..e759f1d2 100644 --- a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.AspNet.Http.Internal; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -18,14 +17,9 @@ public class RequestCookiesFeature : IRequestCookiesFeature, IFeatureCache private IHttpRequestFeature _request; private StringValues _original; - private IReadableStringCollection _parsedValues; - - public RequestCookiesFeature(IDictionary cookies) - : this(new ReadableStringCollection(cookies)) - { - } - - public RequestCookiesFeature(IReadableStringCollection cookies) + private IRequestCookieCollection _parsedValues; + + public RequestCookiesFeature(IRequestCookieCollection cookies) { if (cookies == null) { @@ -59,32 +53,30 @@ private IHttpRequestFeature HttpRequestFeature get { return FeatureHelpers.GetAndCache(this, _features, ref _request); } } - public IReadableStringCollection Cookies + public IRequestCookieCollection Cookies { get { if (_features == null) { - return _parsedValues ?? ReadableStringCollection.Empty; + if (_parsedValues == null) + { + _parsedValues = RequestCookieCollection.Empty; + } + return _parsedValues; } var headers = HttpRequestFeature.Headers; StringValues current; if (!headers.TryGetValue(HeaderNames.Cookie, out current)) { - current = StringValues.Empty; + current = string.Empty; } - if (_parsedValues == null || !Enumerable.SequenceEqual(_original, current, StringComparer.Ordinal)) + if (_parsedValues == null || _original != current) { _original = current; - var collectionParser = _parsedValues as RequestCookiesCollection; - if (collectionParser == null) - { - collectionParser = new RequestCookiesCollection(); - _parsedValues = collectionParser; - } - collectionParser.Reparse(current); + _parsedValues = RequestCookieCollection.Parse(current.ToArray()); } return _parsedValues; @@ -104,10 +96,7 @@ public IReadableStringCollection Cookies var headers = new List(); foreach (var pair in _parsedValues) { - foreach (var cookieValue in pair.Value) - { - headers.Add(new CookieHeaderValue(pair.Key, cookieValue).ToString()); - } + headers.Add(new CookieHeaderValue(pair.Key, pair.Value).ToString()); } _original = headers.ToArray(); HttpRequestFeature.Headers[HeaderNames.Cookie] = _original; diff --git a/src/Microsoft.AspNet.Http/Features/ResponseCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/ResponseCookiesFeature.cs index d56ba114..dd7607a7 100644 --- a/src/Microsoft.AspNet.Http/Features/ResponseCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/ResponseCookiesFeature.cs @@ -39,7 +39,7 @@ public IResponseCookies Cookies if (_cookiesCollection == null) { var headers = HttpResponseFeature.Headers; - _cookiesCollection = new ResponseCookies(new HeaderDictionary(headers)); + _cookiesCollection = new ResponseCookies(headers); } return _cookiesCollection; } diff --git a/src/Microsoft.AspNet.Http/FormCollection.cs b/src/Microsoft.AspNet.Http/FormCollection.cs index a0e74285..7a7d67c2 100644 --- a/src/Microsoft.AspNet.Http/FormCollection.cs +++ b/src/Microsoft.AspNet.Http/FormCollection.cs @@ -1,7 +1,7 @@ // 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.Generic; using Microsoft.Extensions.Primitives; @@ -10,29 +10,226 @@ namespace Microsoft.AspNet.Http.Internal /// /// Contains the parsed form values. /// - public class FormCollection : ReadableStringCollection, IFormCollection + public class FormCollection : IFormCollection { - public FormCollection(IDictionary store) - : this(store, new FormFileCollection()) + public static readonly FormCollection Empty = new FormCollection(); +#if DNXCORE50 + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); +#else + private static readonly string[] EmptyKeys = new string[0]; + private static readonly StringValues[] EmptyValues = new StringValues[0]; +#endif + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private static IFormFileCollection EmptyFiles = new FormFileCollection(); + + private IFormFileCollection _files; + + private FormCollection() + { + // For static Empty + } + + public FormCollection(Dictionary fields, IFormFileCollection files = null) + { + // can be null + Store = fields; + _files = files; + } + + public IFormFileCollection Files + { + get + { + return _files ?? EmptyFiles; + } + private set { _files = value; } + } + + private Dictionary Store { get; set; } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); } - public FormCollection(IDictionary store, IFormFileCollection files) - : base(store) + /// + /// Returns an struct enumerator that iterates through a collection without boxing and is also used via the interface. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() { - if (store == null) + if (Store == null || Store.Count == 0) { - throw new ArgumentNullException(nameof(store)); + // Non-boxed Enumerator + return EmptyEnumerator; } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } - if (files == null) + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - throw new ArgumentNullException(nameof(files)); + // Non-boxed Enumerator + return EmptyIEnumeratorType; } + // Boxed Enumerator + return Store.GetEnumerator(); + } - Files = files; + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return Store.GetEnumerator(); } - public IFormFileCollection Files { get; } + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } } } diff --git a/src/Microsoft.AspNet.Http/HeaderDictionary.cs b/src/Microsoft.AspNet.Http/HeaderDictionary.cs index 2955cdde..f1fde19a 100644 --- a/src/Microsoft.AspNet.Http/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http/HeaderDictionary.cs @@ -9,123 +9,268 @@ namespace Microsoft.AspNet.Http.Internal { /// - /// Represents a wrapper for owin.RequestHeaders and owin.ResponseHeaders. + /// Represents a wrapper for RequestHeaders and ResponseHeaders. /// public class HeaderDictionary : IHeaderDictionary { - public HeaderDictionary() : this(new Dictionary(StringComparer.OrdinalIgnoreCase)) +#if DNXCORE50 + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); +#else + private static readonly string[] EmptyKeys = new string[0]; + private static readonly StringValues[] EmptyValues = new StringValues[0]; +#endif + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + public HeaderDictionary() + { + } + + public HeaderDictionary(Dictionary store) + { + Store = store; + } + + public HeaderDictionary(int capacity) { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); } + private Dictionary Store { get; set; } + /// - /// Initializes a new instance of the class. + /// Get or sets the associated value from the collection as a single string. /// - /// The underlying data store. - public HeaderDictionary(IDictionary store) + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] { - if (store == null) + get { - throw new ArgumentNullException(nameof(store)); + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; } - Store = store; - } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (StringValues.IsNullOrEmpty(value)) + { + if (Store == null) + { + return; + } - private IDictionary Store { get; set; } + Store.Remove(key); + } + else + { + if (Store == null) + { + Store = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + } + + Store[key] = value; + } + } + } /// - /// Gets an that contains the keys in the ;. + /// Throws KeyNotFoundException if the key is not present. /// - /// An that contains the keys in the . - public ICollection Keys + /// The header name. + /// + StringValues IDictionary.this[string key] { - get { return Store.Keys; } + get { return Store[key]; } + set { this[key] = value; } } /// - /// + /// Gets the number of elements contained in the ;. /// - public ICollection Values + /// The number of elements contained in the . + public int Count { - get { return Store.Values; } + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } } /// - /// Gets the number of elements contained in the ;. + /// Gets a value that indicates whether the is in read-only mode. /// - /// The number of elements contained in the . - public int Count + /// true if the is in read-only mode; otherwise, false. + public bool IsReadOnly + { + get + { + return false; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public ICollection Values { - get { return Store.Count; } + get + { + if (Store == null) + { + return EmptyValues; + } + return Store.Values; + } } /// - /// Gets a value that indicates whether the is in read-only mode. + /// Adds a new list of items to the collection. /// - /// true if the is in read-only mode; otherwise, false. - public bool IsReadOnly + /// The item to add. + public void Add(KeyValuePair item) { - get { return Store.IsReadOnly; } + if (item.Key == null) + { + throw new ArgumentNullException("The key is null"); + } + if (Store == null) + { + Store = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + } + Store.Add(item.Key, item.Value); } /// - /// Get or sets the associated value from the collection as a single string. + /// Adds the given header and values to the collection. /// /// The header name. - /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. - public StringValues this[string key] + /// The header values. + public void Add(string key, StringValues value) { - get { return ParsingHelpers.GetHeader(Store, key); } - set { ParsingHelpers.SetHeader(Store, key, value); } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Store == null) + { + Store = new Dictionary(1); + } + Store.Add(key, value); } /// - /// Throws KeyNotFoundException if the key is not present. + /// Clears the entire list of objects. /// - /// The header name. - /// - StringValues IDictionary.this[string key] + public void Clear() { - get { return Store[key]; } - set { Store[key] = value; } + if (Store == null) + { + return; + } + Store.Clear(); } /// - /// Returns an enumerator that iterates through a collection. + /// Returns a value indicating whether the specified object occurs within this collection. /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() + /// The item. + /// true if the specified object occurs within this collection; otherwise, false. + public bool Contains(KeyValuePair item) { - return Store.GetEnumerator(); + StringValues value; + if (Store == null || + !Store.TryGetValue(item.Key, out value) || + !StringValues.Equals(value, item.Value)) + { + return false; + } + return true; } /// - /// Returns an enumerator that iterates through a collection. + /// Determines whether the contains a specific key. /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) { - return Store.GetEnumerator(); + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); } /// - /// Adds the given header and values to the collection. + /// Copies the elements to a one-dimensional Array instance at the specified index. /// - /// The header name. - /// The header values. - public void Add(string key, StringValues value) + /// The one-dimensional Array that is the destination of the specified objects copied from the . + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) { - Store.Add(key, value); + if (Store == null) + { + return; + } + + foreach (var item in Store) + { + array[arrayIndex] = item; + arrayIndex++; + } } /// - /// Determines whether the contains a specific key. + /// Removes the given item from the the collection. /// - /// The key. - /// true if the contains a specific key; otherwise, false. - public bool ContainsKey(string key) + /// The item. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(KeyValuePair item) { - return Store.ContainsKey(key); + if (Store == null) + { + return false; + } + + StringValues value; + + if (Store.TryGetValue(item.Key, out value) && StringValues.Equals(item.Value, value)) + { + return Store.Remove(item.Key); + } + return false; } /// @@ -135,6 +280,10 @@ public bool ContainsKey(string key) /// true if the specified object was removed from the collection; otherwise, false. public bool Remove(string key) { + if (Store == null) + { + return false; + } return Store.Remove(key); } @@ -143,57 +292,111 @@ public bool Remove(string key) /// /// The header name. /// The value. - /// true if the contains the key; otherwise, false. + /// true if the contains the key; otherwise, false. public bool TryGetValue(string key, out StringValues value) { + if (Store == null) + { + value = default(StringValues); + return false; + } return Store.TryGetValue(key, out value); } /// - /// Adds a new list of items to the collection. + /// Returns an enumerator that iterates through a collection. /// - /// The item to add. - public void Add(KeyValuePair item) + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() { - Store.Add(item); + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); } /// - /// Clears the entire list of objects. + /// Returns an enumerator that iterates through a collection. /// - public void Clear() + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() { - Store.Clear(); + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); } /// - /// Returns a value indicating whether the specified object occurs within this collection. + /// Returns an enumerator that iterates through a collection. /// - /// The item. - /// true if the specified object occurs within this collection; otherwise, false. - public bool Contains(KeyValuePair item) + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() { - return Store.Contains(item); + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); } - /// - /// Copies the elements to a one-dimensional Array instance at the specified index. - /// - /// The one-dimensional Array that is the destination of the specified objects copied from the . - /// The zero-based index in at which copying begins. - public void CopyTo(KeyValuePair[] array, int arrayIndex) + public struct Enumerator : IEnumerator> { - Store.CopyTo(array, arrayIndex); - } + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; - /// - /// Removes the given item from the the collection. - /// - /// The item. - /// true if the specified object was removed from the collection; otherwise, false. - public bool Remove(KeyValuePair item) - { - return Store.Remove(item); + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } } } } diff --git a/src/Microsoft.AspNet.Http/ItemsDictionary.cs b/src/Microsoft.AspNet.Http/ItemsDictionary.cs index 90e9cd31..87a27b8b 100644 --- a/src/Microsoft.AspNet.Http/ItemsDictionary.cs +++ b/src/Microsoft.AspNet.Http/ItemsDictionary.cs @@ -97,7 +97,12 @@ bool ICollection>.IsReadOnly bool ICollection>.Remove(KeyValuePair item) { - return Items.Remove(item); + object value; + if (Items.TryGetValue(item.Key, out value) && Equals(item.Value, value)) + { + return Items.Remove(item.Key); + } + return false; } IEnumerator> IEnumerable>.GetEnumerator() diff --git a/src/Microsoft.AspNet.Http/ParsingHelpers.cs b/src/Microsoft.AspNet.Http/ParsingHelpers.cs index 9f6feb3d..8b575fd6 100644 --- a/src/Microsoft.AspNet.Http/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.Http/ParsingHelpers.cs @@ -87,7 +87,7 @@ public HeaderSegmentCollection(StringValues headers) public bool Equals(HeaderSegmentCollection other) { - return Equals(_headers, other._headers); + return StringValues.Equals(_headers, other._headers); } public override bool Equals(object obj) @@ -363,13 +363,7 @@ public void Reset() internal static class ParsingHelpers { - public static StringValues GetHeader(IDictionary headers, string key) - { - StringValues value; - return headers.TryGetValue(key, out value) ? value : StringValues.Empty; - } - - public static StringValues GetHeaderSplit(IDictionary headers, string key) + public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) { var values = GetHeaderUnmodified(headers, key); return new StringValues(GetHeaderSplitImplementation(values).ToArray()); @@ -386,7 +380,7 @@ private static IEnumerable GetHeaderSplitImplementation(StringValues val } } - public static StringValues GetHeaderUnmodified(IDictionary headers, string key) + public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) { if (headers == null) { @@ -397,32 +391,6 @@ public static StringValues GetHeaderUnmodified(IDictionary return headers.TryGetValue(key, out values) ? values : StringValues.Empty; } - public static void SetHeader(IDictionary headers, string key, StringValues value) - { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } - - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if (string.IsNullOrWhiteSpace(key)) - { - throw new ArgumentNullException(nameof(key)); - } - if (StringValues.IsNullOrEmpty(value)) - { - headers.Remove(key); - } - else - { - headers[key] = value; - } - } - private static string DeQuote(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/Microsoft.AspNet.Http/QueryCollection.cs b/src/Microsoft.AspNet.Http/QueryCollection.cs new file mode 100644 index 00000000..2b9168d5 --- /dev/null +++ b/src/Microsoft.AspNet.Http/QueryCollection.cs @@ -0,0 +1,227 @@ +// 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.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNet.Http.Internal +{ + /// + /// The HttpRequest query string collection + /// + public class QueryCollection : IQueryCollection + { + public static readonly QueryCollection Empty = new QueryCollection(); +#if DNXCORE50 + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); +#else + private static readonly string[] EmptyKeys = new string[0]; + private static readonly StringValues[] EmptyValues = new StringValues[0]; +#endif + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary Store { get; set; } + + public QueryCollection() + { + } + + public QueryCollection(Dictionary store) + { + Store = store; + } + + public QueryCollection(QueryCollection store) + { + Store = store.Store; + } + + public QueryCollection(int capacity) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Http/ReadableStringCollection.cs b/src/Microsoft.AspNet.Http/ReadableStringCollection.cs deleted file mode 100644 index 8239699e..00000000 --- a/src/Microsoft.AspNet.Http/ReadableStringCollection.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNet.Http.Internal -{ - /// - /// Accessors for query, forms, etc. - /// - public class ReadableStringCollection : IReadableStringCollection - { - public static readonly IReadableStringCollection Empty = new ReadableStringCollection(new Dictionary(0)); - - /// - /// Create a new wrapper - /// - /// - public ReadableStringCollection(IDictionary store) - { - if (store == null) - { - throw new ArgumentNullException(nameof(store)); - } - - Store = store; - } - - private IDictionary Store { get; set; } - - /// - /// Gets the number of elements contained in the collection. - /// - public int Count - { - get { return Store.Count; } - } - - /// - /// Gets a collection containing the keys. - /// - public ICollection Keys - { - get { return Store.Keys; } - } - - - /// - /// Get the associated value from the collection. Multiple values will be merged. - /// Returns StringValues.Empty if the key is not present. - /// - /// - /// - public StringValues this[string key] - { - get - { - StringValues value; - if (Store.TryGetValue(key, out value)) - { - return value; - } - return StringValues.Empty; - } - } - - /// - /// Determines whether the collection contains an element with the specified key. - /// - /// - /// - public bool ContainsKey(string key) - { - return Store.ContainsKey(key); - } - - - /// - /// - /// - /// - public IEnumerator> GetEnumerator() - { - return Store.GetEnumerator(); - } - - /// - /// - /// - /// - IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/src/Microsoft.AspNet.Http/ReferenceReadStream.cs b/src/Microsoft.AspNet.Http/ReferenceReadStream.cs index 1a413d42..2d6cfdac 100644 --- a/src/Microsoft.AspNet.Http/ReferenceReadStream.cs +++ b/src/Microsoft.AspNet.Http/ReferenceReadStream.cs @@ -61,7 +61,7 @@ public override long Position ThrowIfDisposed(); if (value < 0 || value > Length) { - throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be within the length of the Stream: " + Length); + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be within the length of the Stream: " + Length.ToString()); } VerifyPosition(); _position = value; diff --git a/src/Microsoft.AspNet.Http/RequestCookieCollection.cs b/src/Microsoft.AspNet.Http/RequestCookieCollection.cs new file mode 100644 index 00000000..363b69df --- /dev/null +++ b/src/Microsoft.AspNet.Http/RequestCookieCollection.cs @@ -0,0 +1,235 @@ +// 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.Generic; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Http.Internal +{ + public class RequestCookieCollection : IRequestCookieCollection + { + public static readonly RequestCookieCollection Empty = new RequestCookieCollection(); +#if DNXCORE50 + private static readonly string[] EmptyKeys = Array.Empty(); +#else + private static readonly string[] EmptyKeys = new string[0]; +#endif + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary Store { get; set; } + + public RequestCookieCollection() + { + } + + public RequestCookieCollection(Dictionary store) + { + Store = store; + } + + public RequestCookieCollection(int capacity) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Store == null) + { + return string.Empty; + } + + string value; + if (TryGetValue(key, out value)) + { + return value; + } + return string.Empty; + } + } + + public static RequestCookieCollection Parse(IList values) + { + if (values.Count == 0) + { + return Empty; + } + + IList cookies; + if (CookieHeaderValue.TryParseList(values, out cookies)) + { + if (cookies.Count == 0) + { + return Empty; + } + + var store = new Dictionary(cookies.Count); + for (var i = 0; i < cookies.Count; i++) + { + var cookie = cookies[i]; + var name = Uri.UnescapeDataString(cookie.Name.Replace('+', ' ')); + var value = Uri.UnescapeDataString(cookie.Value.Replace('+', ' ')); + store[name] = value; + } + + return new RequestCookieCollection(store); + } + return Empty; + } + + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + public bool TryGetValue(string key, out string value) + { + if (Store == null) + { + value = string.Empty; + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an struct enumerator that iterates through a collection without boxing. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + var current = _dictionaryEnumerator.Current; + return new KeyValuePair(current.Key, current.Value); + } + return default(KeyValuePair); + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public void Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs b/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs deleted file mode 100644 index d0001c1a..00000000 --- a/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs +++ /dev/null @@ -1,105 +0,0 @@ -// 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.Generic; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNet.Http.Internal -{ - public class RequestCookiesCollection : IReadableStringCollection - { - private readonly IDictionary _dictionary; - - public RequestCookiesCollection() - { - _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public StringValues this[string key] - { - get { return Get(key); } - } - - /// - /// Gets the number of elements contained in the collection. - /// - public int Count - { - get { return _dictionary.Count; } - } - - /// - /// Gets a collection containing the keys. - /// - public ICollection Keys - { - get { return _dictionary.Keys; } - } - - /// - /// Determines whether the collection contains an element with the specified key. - /// - /// - /// - public bool ContainsKey(string key) - { - return _dictionary.ContainsKey(key); - } - - /// - /// Get the associated value from the collection. Multiple values will be merged. - /// Returns null if the key is not present. - /// - /// - /// - public string Get(string key) - { - string value; - return _dictionary.TryGetValue(key, out value) ? value : null; - } - - /// - /// Get the associated values from the collection in their original format. - /// Returns null if the key is not present. - /// - /// - /// - public IList GetValues(string key) - { - string value; - return _dictionary.TryGetValue(key, out value) ? new[] { value } : null; - } - - public void Reparse(IList values) - { - _dictionary.Clear(); - - IList cookies; - if (CookieHeaderValue.TryParseList(values, out cookies)) - { - foreach (var cookie in cookies) - { - var name = Uri.UnescapeDataString(cookie.Name.Replace('+', ' ')); - var value = Uri.UnescapeDataString(cookie.Value.Replace('+', ' ')); - _dictionary[name] = value; - } - } - } - - public IEnumerator> GetEnumerator() - { - foreach (var pair in _dictionary) - { - yield return new KeyValuePair(pair.Key, pair.Value); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/ResponseCookies.cs b/src/Microsoft.AspNet.Http/ResponseCookies.cs index 5075efb0..ed76dec8 100644 --- a/src/Microsoft.AspNet.Http/ResponseCookies.cs +++ b/src/Microsoft.AspNet.Http/ResponseCookies.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; using System.Text.Encodings.Web; +using System.Collections.Generic; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -80,10 +80,10 @@ public void Append(string key, string value, CookieOptions options) /// public void Delete(string key) { - var encodedKeyPlusEquals = UrlEncoder.Default.Encode(key) + "="; - Func predicate = value => value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase); + var encodedKeyPlusEquals = $"{UrlEncoder.Default.Encode(key)}="; + Func predicate = (value, encKeyPlusEquals) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); - StringValues deleteCookies = encodedKeyPlusEquals + "; expires=Thu, 01-Jan-1970 00:00:00 GMT"; + StringValues deleteCookies = $"{encodedKeyPlusEquals}; expires=Thu, 01-Jan-1970 00:00:00 GMT"; var existingValues = Headers[HeaderNames.SetCookie]; if (StringValues.IsNullOrEmpty(existingValues)) { @@ -91,7 +91,24 @@ public void Delete(string key) } else { - Headers[HeaderNames.SetCookie] = existingValues.Where(value => !predicate(value)).Concat(deleteCookies).ToArray(); + var values = existingValues.ToArray(); + var newValues = new List(); + + for (var i = 0; i < values.Length; i++) + { + if (!predicate(values[i], encodedKeyPlusEquals)) + { + newValues.Add(values[i]); + } + } + + values = deleteCookies.ToArray(); + for (var i = 0; i < values.Length; i++) + { + newValues.Add(values[i]); + } + + Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); } } @@ -106,33 +123,44 @@ public void Delete(string key, CookieOptions options) { throw new ArgumentNullException(nameof(options)); } - - var encodedKeyPlusEquals = UrlEncoder.Default.Encode(key) + "="; + + var encodedKeyPlusEquals = $"{UrlEncoder.Default.Encode(key)}="; bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); - Func rejectPredicate; + Func rejectPredicate; if (domainHasValue) { - rejectPredicate = value => - value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && - value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1; } else if (pathHasValue) { - rejectPredicate = value => - value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && - value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; } else { - rejectPredicate = value => value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase); + rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); } var existingValues = Headers[HeaderNames.SetCookie]; if (!StringValues.IsNullOrEmpty(existingValues)) { - Headers[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray(); + var values = existingValues.ToArray(); + var newValues = new List(); + + for (var i = 0; i < values.Length; i++) + { + if (!rejectPredicate(values[i], encodedKeyPlusEquals, options)) + { + newValues.Add(values[i]); + } + } + + Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); } Append(key, string.Empty, new CookieOptions diff --git a/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs index d78d3b34..ccb58d66 100644 --- a/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs +++ b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs @@ -262,7 +262,7 @@ public bool EnsureBuffered(int minCount) { if (minCount > _buffer.Length) { - throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); } while (_bufferCount < minCount) { @@ -289,7 +289,7 @@ public async Task EnsureBufferedAsync(int minCount, CancellationToken canc { if (minCount > _buffer.Length) { - throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); } while (_bufferCount < minCount) { @@ -315,38 +315,42 @@ public async Task EnsureBufferedAsync(int minCount, CancellationToken canc public string ReadLine(int lengthLimit) { CheckDisposed(); - var builder = new MemoryStream(200); - bool foundCR = false, foundCRLF = false; - - while (!foundCRLF && EnsureBuffered()) + using (var builder = new MemoryStream(200)) { - if (builder.Length > lengthLimit) + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && EnsureBuffered()) { - throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit); + if (builder.Length > lengthLimit) + { + throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit.ToString()); + } + ProcessLineChar(builder, ref foundCR, ref foundCRLF); } - ProcessLineChar(builder, ref foundCR, ref foundCRLF); - } - return DecodeLine(builder, foundCRLF); + return DecodeLine(builder, foundCRLF); + } } public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) { CheckDisposed(); - var builder = new MemoryStream(200); - bool foundCR = false, foundCRLF = false; - - while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + using (var builder = new MemoryStream(200)) { - if (builder.Length > lengthLimit) + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) { - throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit); + if (builder.Length > lengthLimit) + { + throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit.ToString()); + } + + ProcessLineChar(builder, ref foundCR, ref foundCRLF); } - ProcessLineChar(builder, ref foundCR, ref foundCRLF); + return DecodeLine(builder, foundCRLF); } - - return DecodeLine(builder, foundCRLF); } private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) diff --git a/src/Microsoft.AspNet.WebUtilities/FormReader.cs b/src/Microsoft.AspNet.WebUtilities/FormReader.cs index e361bac1..554b86e7 100644 --- a/src/Microsoft.AspNet.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNet.WebUtilities/FormReader.cs @@ -185,7 +185,7 @@ public static IDictionary ReadForm(string text) /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) + public static Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) { return ReadFormAsync(stream, Encoding.UTF8, cancellationToken); } @@ -195,7 +195,7 @@ public static IDictionary ReadForm(string text) /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) + public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) { var reader = new FormReader(stream, encoding); diff --git a/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs index c0ecc8c4..9e8f0555 100644 --- a/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs +++ b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs @@ -7,17 +7,16 @@ namespace Microsoft.AspNet.WebUtilities { - public class KeyValueAccumulator + public struct KeyValueAccumulator { private Dictionary> _accumulator; - public KeyValueAccumulator() - { - _accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } - public void Append(string key, string value) { + if (_accumulator == null) + { + _accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } List values; if (_accumulator.TryGetValue(key, out values)) { @@ -25,18 +24,29 @@ public void Append(string key, string value) } else { - _accumulator[key] = new List(1) { value }; + values = new List(1); + values.Add(value); + _accumulator[key] = values; } } - public IDictionary GetResults() + public bool HasValues => _accumulator != null; + + public Dictionary GetResults() { - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (_accumulator == null) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + var results = new Dictionary(_accumulator.Count, StringComparer.OrdinalIgnoreCase); + foreach (var kv in _accumulator) { - results.Add(kv.Key, kv.Value.ToArray()); + results.Add(kv.Key, kv.Value.Count == 1 ? new StringValues(kv.Value[0]) : new StringValues(kv.Value.ToArray())); } + return results; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs index ef6f03a9..e9e0aba9 100644 --- a/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs +++ b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs @@ -74,7 +74,7 @@ public MultipartReader(string boundary, Stream stream, int bufferSize) return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; } - private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) { int totalSize = 0; var accumulator = new KeyValueAccumulator(); @@ -84,10 +84,10 @@ private async Task> ReadHeadersAsync(Cancellat totalSize += line.Length; if (totalSize > TotalHeaderSizeLimit) { - throw new InvalidOperationException("Total header size limit exceeded: " + TotalHeaderSizeLimit); + throw new InvalidOperationException("Total header size limit exceeded: " + TotalHeaderSizeLimit.ToString()); } int splitIndex = line.IndexOf(':'); - Debug.Assert(splitIndex > 0, "Invalid header line: " + line); + Debug.Assert(splitIndex > 0, $"Invalid header line: {line.ToString()}"); if (splitIndex >= 0) { var name = line.Substring(0, splitIndex); diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs index 29dfc495..f3a6e5cc 100644 --- a/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs +++ b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs @@ -16,7 +16,7 @@ public string ContentType StringValues values; if (Headers.TryGetValue("Content-Type", out values)) { - return values; + return values.ToString(); } return null; } @@ -29,13 +29,13 @@ public string ContentDisposition StringValues values; if (Headers.TryGetValue("Content-Disposition", out values)) { - return values; + return values.ToString(); } return null; } } - public IDictionary Headers { get; set; } + public Dictionary Headers { get; set; } public Stream Body { get; set; } diff --git a/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs b/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs index 57ee90bc..036ed557 100644 --- a/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs +++ b/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs @@ -107,21 +107,46 @@ private static string AddQueryString( /// /// The raw query string value, with or without the leading '?'. /// A collection of parsed keys and values. - public static IDictionary ParseQuery(string queryString) + public static Dictionary ParseQuery(string queryString) { - if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?') + var result = ParseNullableQuery(queryString); + + if (result == null) { - queryString = queryString.Substring(1); + return new Dictionary(); } + + return result; + } + + + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values, null if there are no entries. + public static Dictionary ParseNullableQuery(string queryString) + { var accumulator = new KeyValueAccumulator(); + if (string.IsNullOrEmpty(queryString) || queryString == "?") + { + return null; + } + + int scanIndex = 0; + if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?') + { + scanIndex = 1; + } + + int textLength = queryString.Length; int equalIndex = queryString.IndexOf('='); if (equalIndex == -1) { equalIndex = textLength; } - int scanIndex = 0; while (scanIndex < textLength) { int delimiterIndex = queryString.IndexOf('&', scanIndex); @@ -149,6 +174,11 @@ public static IDictionary ParseQuery(string queryString) scanIndex = delimiterIndex + 1; } + if (!accumulator.HasValues) + { + return null; + } + return accumulator.GetResults(); } } diff --git a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs index e020da61..586b8ca1 100644 --- a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs @@ -150,7 +150,7 @@ public void Query_GetAndSet() Assert.Equal("value0", query1["name0"]); Assert.Equal("value1", query1["name1"]); - var query2 = new ReadableStringCollection(new Dictionary() + var query2 = new QueryCollection( new Dictionary() { { "name2", "value2" } }); @@ -170,19 +170,23 @@ public void Cookies_GetAndSet() var cookies0 = request.Cookies; Assert.Equal(0, cookies0.Count); - request.Headers["Cookie"] = new[] { "name0=value0", "name1=value1" }; + var newCookies = new[] { "name0=value0", "name1=value1" }; + request.Headers["Cookie"] = newCookies; + + cookies0 = RequestCookieCollection.Parse(newCookies); var cookies1 = request.Cookies; - Assert.Same(cookies0, cookies1); + Assert.Equal(cookies0, cookies1); Assert.Equal(2, cookies1.Count); Assert.Equal("value0", cookies1["name0"]); Assert.Equal("value1", cookies1["name1"]); + Assert.Equal(newCookies, request.Headers["Cookie"]); - var cookies2 = new ReadableStringCollection(new Dictionary() + var cookies2 = new RequestCookieCollection(new Dictionary() { { "name2", "value2" } }); request.Cookies = cookies2; - Assert.Same(cookies2, request.Cookies); + Assert.Equal(cookies2, request.Cookies); Assert.Equal("value2", request.Cookies["name2"]); cookieHeaders = request.Headers["Cookie"]; Assert.Equal(new[] { "name2=value2" }, cookieHeaders);