From 087080ad7e49e43c6a37a4b53d90158ba670ac3c Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 17 Aug 2015 21:53:09 -0700 Subject: [PATCH] #361 Introduce StringValues to replace string[] usage. --- .../IHeaderDictionary.cs | 40 +-- .../IReadableStringCollection.cs | 26 +- .../HeaderDictionaryTypeExtensions.cs | 12 +- .../RequestHeaders.cs | 2 +- .../ResponseHeaders.cs | 2 +- .../IHttpRequestFeature.cs | 2 +- .../IHttpResponseFeature.cs | 2 +- .../StringValues.cs | 230 ++++++++++++++++++ .../project.json | 5 +- .../DefaultHttpResponse.cs | 7 +- .../Features/FormFeature.cs | 11 +- .../Features/HttpRequestFeature.cs | 4 +- .../Features/HttpResponseFeature.cs | 4 +- .../Features/QueryFeature.cs | 42 ++-- .../Features/RequestCookiesFeature.cs | 65 +++-- src/Microsoft.AspNet.Http/FormCollection.cs | 4 +- src/Microsoft.AspNet.Http/HeaderDictionary.cs | 96 ++------ src/Microsoft.AspNet.Http/ParsingHelpers.cs | 102 +++----- .../ReadableStringCollection.cs | 55 ++--- .../RequestCookiesCollection.cs | 6 +- src/Microsoft.AspNet.Http/ResponseCookies.cs | 44 ++-- .../DictionaryStringArrayWrapper.cs | 79 ++++++ .../DictionaryStringValuesWrapper.cs | 79 ++++++ src/Microsoft.AspNet.Owin/OwinEnvironment.cs | 4 +- .../OwinFeatureCollection.cs | 13 +- src/Microsoft.AspNet.Owin/Utilities.cs | 25 +- .../StringValuesTests.cs | 132 ++++++++++ .../DefaultHttpRequestTests.cs | 32 +-- .../HeaderDictionaryTests.cs | 7 +- 29 files changed, 784 insertions(+), 348 deletions(-) create mode 100644 src/Microsoft.AspNet.Http.Features/StringValues.cs create mode 100644 src/Microsoft.AspNet.Owin/DictionaryStringArrayWrapper.cs create mode 100644 src/Microsoft.AspNet.Owin/DictionaryStringValuesWrapper.cs create mode 100644 test/Microsoft.AspNet.Http.Features.Tests/StringValuesTests.cs diff --git a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs index b10101cb..8c8baaeb 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs @@ -2,21 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNet.Http { /// /// Represents request and response headers /// - public interface IHeaderDictionary : IReadableStringCollection, IDictionary + public interface IHeaderDictionary : IReadableStringCollection, IDictionary { + // This property is duplicated to resolve an ambiguity between IReadableStringCollection and IDictionary /// - /// Get or sets the associated value from the collection as a single string. + /// /// - /// The header name. - /// the associated value from the collection as a single string or null if the key is not present. - new string this[string key] { get; set; } + /// + /// The stored value, or StringValues.Empty if the key is not present. + new StringValues this[string key] { get; set; } // This property is duplicated to resolve an ambiguity between IReadableStringCollection.Count and IDictionary.Count /// @@ -36,21 +36,14 @@ public interface IHeaderDictionary : IReadableStringCollection, IDictionary /// The header name. /// the associated values from the collection separated into individual values, or null if the key is not present. - IList GetCommaSeparatedValues(string key); + StringValues GetCommaSeparatedValues(string key); /// - /// Add a new value. Appends to the header if already present + /// Add a new value. Appends to the header list if already present /// /// The header name. /// The header value. - void Append(string key, string value); - - /// - /// Add new values. Each item remains a separate array entry. - /// - /// The header name. - /// The header values. - void AppendValues(string key, params string[] values); + void Append(string key, StringValues value); /// /// Quotes any values containing comas, and then coma joins all of the values with any existing values. @@ -59,21 +52,6 @@ public interface IHeaderDictionary : IReadableStringCollection, IDictionaryThe header values. void AppendCommaSeparatedValues(string key, params string[] values); - /// - /// Sets a specific header value. - /// - /// The header name. - /// The header value. - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification = "Re-evaluate later.")] - void Set(string key, string value); - - /// - /// Sets the specified header values without modification. - /// - /// The header name. - /// The header values. - void SetValues(string key, params string[] values); - /// /// Quotes any values containing comas, and then coma joins all of the values. /// diff --git a/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs b/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs index 589b7588..a93c3f40 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs @@ -2,22 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNet.Http { /// /// Accessors for headers, query, forms, etc. /// - public interface IReadableStringCollection : IEnumerable> + public interface IReadableStringCollection : IEnumerable> { /// - /// Get the associated value from the collection. Multiple values will be merged. - /// Returns null if the key is not present. + /// Get the associated value from the collection. + /// Returns StringValues.Empty if the key is not present. /// /// /// - string this[string key] { get; } + StringValues this[string key] { get; } /// /// Gets the number of elements contained in the collection. @@ -35,22 +34,5 @@ public interface IReadableStringCollection : IEnumerable /// bool ContainsKey(string key); - - /// - /// Get the associated value from the collection. Multiple values will be merged. - /// Returns null if the key is not present. - /// - /// - /// - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", Justification = "Re-evaluate later.")] - string Get(string key); - - /// - /// Get the associated values from the collection in their original format. - /// Returns null if the key is not present. - /// - /// - /// - IList GetValues(string key); } } diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs index 69f94e98..d8d28331 100644 --- a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs @@ -50,7 +50,7 @@ internal static void SetList([NotNull] this IHeaderDictionary headers, [NotNu } else { - headers.SetValues(name, values.Select(value => value.ToString()).ToArray()); + headers[name] = values.Select(value => value.ToString()).ToArray(); } } @@ -98,7 +98,7 @@ internal static T Get([NotNull] this IHeaderDictionary headers, string name) } var value = headers[name]; - if (string.IsNullOrWhiteSpace(value)) + if (StringValues.IsNullOrEmpty(value)) { return default(T); } @@ -112,11 +112,11 @@ internal static IList GetList([NotNull] this IHeaderDictionary headers, st if (KnownListParsers.TryGetValue(typeof(T), out temp)) { var func = (Func, IList>)temp; - return func(headers.GetValues(name)); + return func(headers[name]); } - var values = headers.GetValues(name); - if (values == null || !values.Any()) + var values = headers[name]; + if (StringValues.IsNullOrEmpty(values)) { return null; } @@ -158,7 +158,7 @@ private static T GetViaReflection(string value) return default(T); } - private static IList GetListViaReflection(IList values) + private static IList GetListViaReflection(StringValues values) { // TODO: Cache the reflected type for later? Only if success? var type = typeof(T); diff --git a/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs index ac9e5425..c718ada9 100644 --- a/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs +++ b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs @@ -285,7 +285,7 @@ public void Append([NotNull] string name, [NotNull] object value) public void AppendList([NotNull] string name, [NotNull] IList values) { - Headers.AppendValues(name, values.Select(value => value.ToString()).ToArray()); + Headers.Append(name, values.Select(value => value.ToString()).ToArray()); } } } \ 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 8b8607a2..7ee2bb79 100644 --- a/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs +++ b/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs @@ -182,7 +182,7 @@ public void Append([NotNull] string name, [NotNull] object value) public void AppendList([NotNull] string name, [NotNull] IList values) { - Headers.AppendValues(name, values.Select(value => value.ToString()).ToArray()); + Headers.Append(name, values.Select(value => value.ToString()).ToArray()); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs b/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs index c80ca29b..eec5e0d0 100644 --- a/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs @@ -14,7 +14,7 @@ public interface IHttpRequestFeature string PathBase { get; set; } string Path { get; set; } string QueryString { get; set; } - IDictionary Headers { get; set; } + IDictionary Headers { get; set; } Stream Body { get; set; } } } diff --git a/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs b/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs index d530718b..b5947ee4 100644 --- a/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs @@ -12,7 +12,7 @@ public interface IHttpResponseFeature { int StatusCode { get; set; } string ReasonPhrase { get; set; } - IDictionary Headers { get; set; } + IDictionary Headers { get; set; } Stream Body { get; set; } bool HasStarted { get; } void OnStarting(Func callback, object state); diff --git a/src/Microsoft.AspNet.Http.Features/StringValues.cs b/src/Microsoft.AspNet.Http.Features/StringValues.cs new file mode 100644 index 00000000..344b55ac --- /dev/null +++ b/src/Microsoft.AspNet.Http.Features/StringValues.cs @@ -0,0 +1,230 @@ +// 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; + +// TODO: The location of this file/type is concerning. It is the only 'primitive' type in Features. +// compare/contrast with similar wrappers like PathString. +namespace Microsoft.AspNet.Http +{ + /// + /// Represents zero/null, one, or many strings in an efficient way. + /// + public struct StringValues : IList + { + private static readonly string[] EmptyArray = new string[0]; + public static readonly StringValues Empty = new StringValues(EmptyArray); + + private readonly string _value; + private readonly string[] _values; + + public StringValues(string value) + { + _value = value; + _values = null; + } + + public StringValues(string[] values) + { + _value = null; + _values = values; + } + + public static implicit operator StringValues(string value) + { + return new StringValues(value); + } + + public static implicit operator StringValues(string[] values) + { + return new StringValues(values); + } + + public static implicit operator string (StringValues values) + { + return values.GetStringValue(); + } + + public static implicit operator string[] (StringValues value) + { + if (value._values != null) + { + return value._values; + } + if (value._value != null) + { + return new string[1] { value._value }; + } + return EmptyArray; + } + + public int Count => _values?.Length ?? (_value != null ? 1 : 0); + + bool ICollection.IsReadOnly + { + get { return true; } + } + + string IList.this[int index] + { + get { return this[index]; } + set { throw new NotSupportedException(); } + } + + public string this[int key] + { + get + { + if (_values != null) + { + return _values[key]; // may throw + } + if (key == 0 && _value != null) + { + return _value; + } + return EmptyArray[0]; // throws + } + } + + public override string ToString() + { + return GetStringValue() ?? string.Empty; + } + + private string GetStringValue() + { + if (_values == null) + { + return _value; + } + switch (_values.Length) + { + case 0: return null; + case 1: return _values[0]; + default: return string.Join(",", _values); + } + } + + int IList.IndexOf(string item) + { + var index = 0; + foreach (var value in this) + { + if (string.Equals(value, item, StringComparison.Ordinal)) + { + return index; + } + index += 1; + } + return -1; + } + + bool ICollection.Contains(string item) + { + return ((IList)this).IndexOf(item) >= 0; + } + + void ICollection.CopyTo(string[] array, int arrayIndex) + { + for(int i = 0; i < Count; i++) + { + array[arrayIndex + i] = this[i]; + } + } + + void ICollection.Add(string item) + { + throw new NotSupportedException(); + } + + void IList.Insert(int index, string item) + { + throw new NotSupportedException(); + } + + bool ICollection.Remove(string item) + { + throw new NotSupportedException(); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (_values == null) + { + yield return _value; + } + else + { + for (int i = 0; i < _values.Length; i++) + { + yield return _values[i]; + } + } + } + + public static bool IsNullOrEmpty(StringValues value) + { + if (value._values == null) + { + return string.IsNullOrEmpty(value._value); + } + switch (value._values.Length) + { + case 0: return true; + case 1: return string.IsNullOrEmpty(value._values[0]); + default: return false; + } + } + + public static StringValues operator +(StringValues values1, StringValues values2) + { + return Concat(values1, values2); + } + + public static StringValues Concat(StringValues values1, StringValues values2) + { + var count1 = values1.Count; + var count2 = values2.Count; + + if (count1 == 0) + { + return values2; + } + + if (count2 == 0) + { + return values1; + } + + var combined = new string[count1 + count2]; + var index = 0; + foreach (var value in values1) + { + combined[index++] = value; + } + foreach (var value in values2) + { + combined[index++] = value; + } + return new StringValues(combined); + } + } +} diff --git a/src/Microsoft.AspNet.Http.Features/project.json b/src/Microsoft.AspNet.Http.Features/project.json index 3af9685e..bfb4a556 100644 --- a/src/Microsoft.AspNet.Http.Features/project.json +++ b/src/Microsoft.AspNet.Http.Features/project.json @@ -6,7 +6,10 @@ "url": "git://github.com/aspnet/httpabstractions" }, "dependencies": { - "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } + "Microsoft.Framework.NotNullAttribute.Sources": { + "type": "build", + "version": "1.0.0-*" + } }, "frameworks": { "dnx451": { }, diff --git a/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs b/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs index 0dae9253..b47279bf 100644 --- a/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNet.Http/DefaultHttpResponse.cs @@ -68,8 +68,7 @@ public override string ContentType { get { - var contentType = Headers[HeaderNames.ContentType]; - return contentType; + return Headers[HeaderNames.ContentType]; } set { @@ -79,7 +78,7 @@ public override string ContentType } else { - HttpResponseFeature.Headers[HeaderNames.ContentType] = new[] { value }; + HttpResponseFeature.Headers[HeaderNames.ContentType] = value; } } } @@ -115,7 +114,7 @@ public override void Redirect(string location, bool permanent) HttpResponseFeature.StatusCode = 302; } - Headers.Set(HeaderNames.Location, location); + Headers[HeaderNames.Location] = location; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/FormFeature.cs b/src/Microsoft.AspNet.Http/Features/FormFeature.cs index 593cd274..e43c5a73 100644 --- a/src/Microsoft.AspNet.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/FormFeature.cs @@ -11,6 +11,7 @@ using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; +using System.Linq; namespace Microsoft.AspNet.Http.Features.Internal { @@ -109,9 +110,9 @@ public async Task ReadFormAsync(CancellationToken cancellationT var section = await multipartReader.ReadNextSectionAsync(cancellationToken); while (section != null) { - var headers = new HeaderDictionary(section.Headers); + var headers = new HeaderDictionary(section.Headers.ToDictionary(kv => kv.Key, kv => new StringValues(kv.Value))); ContentDispositionHeaderValue contentDisposition; - ContentDispositionHeaderValue.TryParse(headers.Get(HeaderNames.ContentDisposition), out contentDisposition); + ContentDispositionHeaderValue.TryParse(headers[HeaderNames.ContentDisposition], out contentDisposition); if (HasFileContentDisposition(contentDisposition)) { // Find the end @@ -131,7 +132,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name); MediaTypeHeaderValue mediaType; - MediaTypeHeaderValue.TryParse(headers.Get(HeaderNames.ContentType), out mediaType); + MediaTypeHeaderValue.TryParse(headers[HeaderNames.ContentType], out mediaType); var encoding = FilterEncoding(mediaType?.Encoding); using (var reader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) { @@ -141,7 +142,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT } else { - System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + headers.Get(HeaderNames.ContentDisposition)); + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + headers[HeaderNames.ContentDisposition]); } section = await multipartReader.ReadNextSectionAsync(cancellationToken); @@ -151,7 +152,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT } } - Form = new FormCollection(formFields, files); + Form = new FormCollection(formFields.ToDictionary(kv => kv.Key, kv => new StringValues(kv.Value)), files); return Form; } diff --git a/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs b/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs index bce75709..d6961289 100644 --- a/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs @@ -11,7 +11,7 @@ public class HttpRequestFeature : IHttpRequestFeature { public HttpRequestFeature() { - Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); Body = Stream.Null; Protocol = string.Empty; Scheme = string.Empty; @@ -27,7 +27,7 @@ public HttpRequestFeature() public string PathBase { get; set; } public string Path { get; set; } public string QueryString { get; set; } - public IDictionary Headers { get; set; } + public IDictionary Headers { get; set; } public Stream Body { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs b/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs index 84f8196f..0b83edc6 100644 --- a/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs @@ -13,7 +13,7 @@ public class HttpResponseFeature : IHttpResponseFeature public HttpResponseFeature() { StatusCode = 200; - Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); Body = Stream.Null; } @@ -21,7 +21,7 @@ public HttpResponseFeature() public string ReasonPhrase { get; set; } - public IDictionary Headers { get; set; } + public IDictionary Headers { get; set; } public Stream Body { get; set; } diff --git a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs index 02545c50..754aa17b 100644 --- a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.Internal; @@ -13,17 +14,19 @@ public class QueryFeature : IQueryFeature { private readonly IFeatureCollection _features; private FeatureReference _request = FeatureReference.Default; - private string _queryString; - private IReadableStringCollection _query; - public QueryFeature([NotNull] IDictionary query) - : this (new ReadableStringCollection(query)) + private string _original; + private IReadableStringCollection _created; + private IReadableStringCollection _assigned; + + public QueryFeature([NotNull] IDictionary query) + : this(new ReadableStringCollection(query)) { } public QueryFeature([NotNull] IReadableStringCollection query) { - _query = query; + _assigned = query; } public QueryFeature([NotNull] IFeatureCollection features) @@ -35,26 +38,35 @@ public IReadableStringCollection Query { get { - if (_features == null) + if (_assigned != null) { - return _query; + return _assigned; } - var queryString = _request.Fetch(_features).QueryString; - if (_query == null || !string.Equals(_queryString, queryString, StringComparison.Ordinal)) + var current = _features == null ? string.Empty : _request.Fetch(_features).QueryString; + if (_created == null || !string.Equals(_original, current, StringComparison.Ordinal)) { - _queryString = queryString; - _query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString)); + _original = current; + _created = new ReadableStringCollection(QueryHelpers.ParseQuery(current).ToDictionary(kv => kv.Key, kv => (StringValues)kv.Value)); } - return _query; + return _created; } set { - _query = value; + _assigned = value; + _created = null; if (_features != null) { - _queryString = _query == null ? string.Empty : QueryString.Create(_query).ToString(); - _request.Fetch(_features).QueryString = _queryString; + if (value == null) + { + _original = string.Empty; + _request.Fetch(_features).QueryString = string.Empty; + } + else + { + _original = QueryString.Create(_assigned.Select(kv => new KeyValuePair(kv.Key, kv.Value))).ToString(); + _request.Fetch(_features).QueryString = _original; + } } } } diff --git a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs index 08317fdd..da06011d 100644 --- a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs @@ -14,18 +14,19 @@ public class RequestCookiesFeature : IRequestCookiesFeature { private readonly IFeatureCollection _features; private readonly FeatureReference _request = FeatureReference.Default; - private string[] _cookieHeaders; - private RequestCookiesCollection _cookiesCollection; - private IReadableStringCollection _cookies; - public RequestCookiesFeature([NotNull] IDictionary cookies) - : this (new ReadableStringCollection(cookies)) + private StringValues _original; + private RequestCookiesCollection _created; + private IReadableStringCollection _assigned; + + public RequestCookiesFeature([NotNull] IDictionary cookies) + : this(new ReadableStringCollection(cookies)) { } public RequestCookiesFeature([NotNull] IReadableStringCollection cookies) { - _cookies = cookies; + _assigned = cookies; } public RequestCookiesFeature([NotNull] IFeatureCollection features) @@ -37,48 +38,58 @@ public IReadableStringCollection Cookies { get { + if (_assigned != null) + { + return _assigned; + } if (_features == null) { - return _cookies; + return ReadableStringCollection.Empty; } var headers = _request.Fetch(_features).Headers; - string[] values; - if (!headers.TryGetValue(HeaderNames.Cookie, out values)) + StringValues current; + if (!headers.TryGetValue(HeaderNames.Cookie, out current)) { - values = new string[0]; + current = StringValues.Empty; } - if (_cookieHeaders == null || !Enumerable.SequenceEqual(_cookieHeaders, values, StringComparer.Ordinal)) + if (_created == null || !Enumerable.SequenceEqual(_original, current, StringComparer.Ordinal)) { - _cookieHeaders = values; - if (_cookiesCollection == null) + _original = current; + if (_created == null) { - _cookiesCollection = new RequestCookiesCollection(); - _cookies = _cookiesCollection; + _created = new RequestCookiesCollection(); } - _cookiesCollection.Reparse(values); + _created.Reparse(current); } - return _cookies; + return _created; } set { - _cookies = value; - _cookieHeaders = null; - _cookiesCollection = _cookies as RequestCookiesCollection; - if (_cookies != null && _features != null) + _assigned = value; + _original = StringValues.Empty; + _created = null; + if (_features != null) { - var headers = new List(); - foreach (var pair in _cookies) + if (_assigned == null || _assigned.Count == 0) + { + _request.Fetch(_features).Headers.Remove(HeaderNames.Cookie); + } + else { - foreach (var cookieValue in pair.Value) + var headers = new List(); + foreach (var pair in _assigned) { - headers.Add(new CookieHeaderValue(pair.Key, cookieValue).ToString()); + foreach (var cookieValue in pair.Value) + { + headers.Add(new CookieHeaderValue(pair.Key, cookieValue).ToString()); + } } + _original = headers.ToArray(); + _request.Fetch(_features).Headers[HeaderNames.Cookie] = _original; } - _cookieHeaders = headers.ToArray(); - _request.Fetch(_features).Headers[HeaderNames.Cookie] = _cookieHeaders; } } } diff --git a/src/Microsoft.AspNet.Http/FormCollection.cs b/src/Microsoft.AspNet.Http/FormCollection.cs index ad829cd7..ddae4c3f 100644 --- a/src/Microsoft.AspNet.Http/FormCollection.cs +++ b/src/Microsoft.AspNet.Http/FormCollection.cs @@ -11,12 +11,12 @@ namespace Microsoft.AspNet.Http.Internal /// public class FormCollection : ReadableStringCollection, IFormCollection { - public FormCollection([NotNull] IDictionary store) + public FormCollection([NotNull] IDictionary store) : this(store, new FormFileCollection()) { } - public FormCollection([NotNull] IDictionary store, [NotNull] IFormFileCollection files) + public FormCollection([NotNull] IDictionary store, [NotNull] IFormFileCollection files) : base(store) { Files = files; diff --git a/src/Microsoft.AspNet.Http/HeaderDictionary.cs b/src/Microsoft.AspNet.Http/HeaderDictionary.cs index 04c2d556..a4d88350 100644 --- a/src/Microsoft.AspNet.Http/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http/HeaderDictionary.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Http.Internal /// public class HeaderDictionary : IHeaderDictionary { - public HeaderDictionary() : this(new Dictionary(StringComparer.OrdinalIgnoreCase)) + public HeaderDictionary() : this(new Dictionary(StringComparer.OrdinalIgnoreCase)) { } @@ -23,12 +23,12 @@ public HeaderDictionary() : this(new Dictionary(StringComparer /// Initializes a new instance of the class. /// /// The underlying data store. - public HeaderDictionary([NotNull] IDictionary store) + public HeaderDictionary([NotNull] IDictionary store) { Store = store; } - private IDictionary Store { get; set; } + private IDictionary Store { get; set; } /// /// Gets an that contains the keys in the ;. @@ -42,7 +42,7 @@ public ICollection Keys /// /// /// - public ICollection Values + public ICollection Values { get { return Store.Values; } } @@ -69,11 +69,11 @@ public bool IsReadOnly /// Get or sets the associated value from the collection as a single string. /// /// The header name. - /// the associated value from the collection as a single string or null if the key is not present. - public string this[string key] + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] { - get { return Get(key); } - set { Set(key, value); } + get { return ParsingHelpers.GetHeader(Store, key); } + set { ParsingHelpers.SetHeader(Store, key, value); } } /// @@ -81,7 +81,7 @@ public string this[string key] /// /// The header name. /// - string[] IDictionary.this[string key] + StringValues IDictionary.this[string key] { get { return Store[key]; } set { Store[key] = value; } @@ -91,7 +91,7 @@ string[] IDictionary.this[string key] /// Returns an enumerator that iterates through a collection. /// /// An object that can be used to iterate through the collection. - public IEnumerator> GetEnumerator() + IEnumerator> IEnumerable>.GetEnumerator() { return Store.GetEnumerator(); } @@ -102,59 +102,29 @@ public IEnumerator> GetEnumerator() /// An object that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - /// - /// Get the associated value from the collection as a single string. - /// - /// The header name. - /// the associated value from the collection as a single string or null if the key is not present. - public string Get(string key) - { - return ParsingHelpers.GetHeader(Store, key); + return Store.GetEnumerator(); } - /// - /// Get the associated values from the collection without modification. - /// - /// The header name. - /// the associated value from the collection without modification, or null if the key is not present. - public IList GetValues(string key) - { - return ParsingHelpers.GetHeaderUnmodified(Store, key); - } /// /// Get the associated values from the collection separated into individual values. /// Quoted values will not be split, and the quotes will be removed. /// /// The header name. - /// the associated values from the collection separated into individual values, or null if the key is not present. - public IList GetCommaSeparatedValues(string key) + /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. + public StringValues GetCommaSeparatedValues(string key) { - IEnumerable values = ParsingHelpers.GetHeaderSplit(Store, key); - return values == null ? null : values.ToList(); - } - - /// - /// Add a new value. Appends to the header if already present - /// - /// The header name. - /// The header value. - public void Append(string key, string value) - { - ParsingHelpers.AppendHeader(Store, key, value); + return ParsingHelpers.GetHeaderSplit(Store, key); } /// /// Add new values. Each item remains a separate array entry. /// /// The header name. - /// The header values. - public void AppendValues(string key, params string[] values) + /// The header value. + public void Append(string key, StringValues value) { - ParsingHelpers.AppendHeaderUnmodified(Store, key, values); + ParsingHelpers.AppendHeaderUnmodified(Store, key, value); } /// @@ -167,26 +137,6 @@ public void AppendCommaSeparatedValues(string key, params string[] values) ParsingHelpers.AppendHeaderJoined(Store, key, values); } - /// - /// Sets a specific header value. - /// - /// The header name. - /// The header value. - public void Set(string key, string value) - { - ParsingHelpers.SetHeader(Store, key, value); - } - - /// - /// Sets the specified header values without modification. - /// - /// The header name. - /// The header values. - public void SetValues(string key, params string[] values) - { - ParsingHelpers.SetHeaderUnmodified(Store, key, values); - } - /// /// Quotes any values containing comas, and then coma joins all of the values. /// @@ -202,7 +152,7 @@ public void SetCommaSeparatedValues(string key, params string[] values) /// /// The header name. /// The header values. - public void Add(string key, string[] value) + public void Add(string key, StringValues value) { Store.Add(key, value); } @@ -233,7 +183,7 @@ public bool Remove(string key) /// The header name. /// The value. /// true if the contains the key; otherwise, false. - public bool TryGetValue(string key, out string[] value) + public bool TryGetValue(string key, out StringValues value) { return Store.TryGetValue(key, out value); } @@ -242,7 +192,7 @@ public bool TryGetValue(string key, out string[] value) /// Adds a new list of items to the collection. /// /// The item to add. - public void Add(KeyValuePair item) + public void Add(KeyValuePair item) { Store.Add(item); } @@ -260,7 +210,7 @@ public void Clear() /// /// The item. /// true if the specified object occurs within this collection; otherwise, false. - public bool Contains(KeyValuePair item) + public bool Contains(KeyValuePair item) { return Store.Contains(item); } @@ -270,7 +220,7 @@ public bool Contains(KeyValuePair item) /// /// 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 void CopyTo(KeyValuePair[] array, int arrayIndex) { Store.CopyTo(array, arrayIndex); } @@ -280,7 +230,7 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) /// /// The item. /// true if the specified object was removed from the collection; otherwise, false. - public bool Remove(KeyValuePair item) + public bool Remove(KeyValuePair item) { return Store.Remove(item); } diff --git a/src/Microsoft.AspNet.Http/ParsingHelpers.cs b/src/Microsoft.AspNet.Http/ParsingHelpers.cs index 45ac370e..64e086a5 100644 --- a/src/Microsoft.AspNet.Http/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.Http/ParsingHelpers.cs @@ -76,9 +76,9 @@ public override int GetHashCode() [System.CodeDom.Compiler.GeneratedCode("App_Packages", "")] internal struct HeaderSegmentCollection : IEnumerable, IEquatable { - private readonly string[] _headers; + private readonly StringValues _headers; - public HeaderSegmentCollection(string[] headers) + public HeaderSegmentCollection(StringValues headers) { _headers = headers; } @@ -102,7 +102,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (_headers != null ? _headers.GetHashCode() : 0); + return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); } public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) @@ -134,7 +134,7 @@ IEnumerator IEnumerable.GetEnumerator() internal struct Enumerator : IEnumerator { - private readonly string[] _headers; + private readonly StringValues _headers; private int _index; private string _header; @@ -149,11 +149,9 @@ internal struct Enumerator : IEnumerator private Mode _mode; - private static readonly string[] NoHeaders = new string[0]; - - public Enumerator(string[] headers) + public Enumerator(StringValues headers) { - _headers = headers ?? NoHeaders; + _headers = headers; _header = string.Empty; _headerLength = -1; _index = -1; @@ -237,7 +235,7 @@ public bool MoveNext() _trailingStart = -1; // if that was the last string - if (_index == _headers.Length) + if (_index == _headers.Count) { // no more move nexts return false; @@ -496,21 +494,21 @@ public override string ToString() internal static class ParsingHelpers { - public static string GetHeader(IDictionary headers, string key) + public static StringValues GetHeader(IDictionary headers, string key) { - string[] values = GetHeaderUnmodified(headers, key); - return values == null ? null : string.Join(",", values); + StringValues value; + return headers.TryGetValue(key, out value) ? value : StringValues.Empty; } - public static IEnumerable GetHeaderSplit(IDictionary headers, string key) + public static StringValues GetHeaderSplit(IDictionary headers, string key) { - string[] values = GetHeaderUnmodified(headers, key); - return values == null ? null : GetHeaderSplitImplementation(values); + var values = GetHeaderUnmodified(headers, key); + return new StringValues(GetHeaderSplitImplementation(values).ToArray()); } - private static IEnumerable GetHeaderSplitImplementation(string[] values) + private static IEnumerable GetHeaderSplitImplementation(StringValues? values) { - foreach (var segment in new HeaderSegmentCollection(values)) + foreach (var segment in new HeaderSegmentCollection(values.Value)) { if (segment.Data.HasValue) { @@ -519,41 +517,41 @@ private static IEnumerable GetHeaderSplitImplementation(string[] values) } } - public static string[] GetHeaderUnmodified([NotNull] IDictionary headers, string key) + public static StringValues? GetHeaderUnmodified([NotNull] IDictionary headers, string key) { - string[] values; - return headers.TryGetValue(key, out values) ? values : null; + StringValues values; + return headers.TryGetValue(key, out values) ? values : (StringValues?)null; } - public static void SetHeader([NotNull] IDictionary headers, [NotNull] string key, string value) + public static void SetHeader([NotNull] IDictionary headers, [NotNull] string key, StringValues value) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentNullException(nameof(key)); } - if (string.IsNullOrWhiteSpace(value)) + if (StringValues.IsNullOrEmpty(value)) { headers.Remove(key); } else { - headers[key] = new[] { value }; + headers[key] = value; } } - public static void SetHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) + public static void SetHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, StringValues value) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentNullException(nameof(key)); } - if (values == null || values.Length == 0) + if (StringValues.IsNullOrEmpty(value)) { headers.Remove(key); } else { - headers[key] = new[] { string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + headers[key] = string.Join(",", value.Select(QuoteIfNeeded)); } } @@ -589,46 +587,23 @@ private static string DeQuote(string value) return value; } - public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, params string[] values) + public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues? values) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentNullException(nameof(key)); } - if (values == null || values.Length == 0) + if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) { headers.Remove(key); } else { - headers[key] = values; + headers[key] = values.Value; } } - public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, [NotNull] IEnumerable values) - { - headers[key] = values.ToArray(); - } - - public static void AppendHeader([NotNull] IDictionary headers, [NotNull] string key, string values) - { - if (string.IsNullOrWhiteSpace(values)) - { - return; - } - - string existing = GetHeader(headers, key); - if (existing == null) - { - SetHeader(headers, key, values); - } - else - { - headers[key] = new[] { existing + "," + values }; - } - } - - public static void AppendHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) + public static void AppendHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) { if (values == null || values.Length == 0) { @@ -642,25 +617,25 @@ public static void AppendHeaderJoined([NotNull] IDictionary he } else { - headers[key] = new[] { existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); } } - public static void AppendHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, params string[] values) + public static void AppendHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues values) { - if (values == null || values.Length == 0) + if (values.Count == 0) { return; } - string[] existing = GetHeaderUnmodified(headers, key); - if (existing == null) + var existing = GetHeaderUnmodified(headers, key); + if (existing.HasValue) { - SetHeaderUnmodified(headers, key, values); + SetHeaderUnmodified(headers, key, existing.Value + values); } else { - SetHeaderUnmodified(headers, key, existing.Concat(values)); + SetHeaderUnmodified(headers, key, values); } } @@ -668,9 +643,10 @@ public static void AppendHeaderUnmodified([NotNull] IDictionary public class ReadableStringCollection : IReadableStringCollection { + public static readonly IReadableStringCollection Empty = new ReadableStringCollection(new Dictionary(0)); + /// /// Create a new wrapper /// /// - public ReadableStringCollection([NotNull] IDictionary store) + public ReadableStringCollection([NotNull] IDictionary store) { Store = store; } - private IDictionary Store { get; set; } + private IDictionary Store { get; set; } /// /// Gets the number of elements contained in the collection. @@ -42,13 +44,21 @@ public ICollection Keys /// /// Get the associated value from the collection. Multiple values will be merged. - /// Returns null if the key is not present. + /// Returns StringValues.Empty if the key is not present. /// /// /// - public string this[string key] + public StringValues this[string key] { - get { return Get(key); } + get + { + StringValues value; + if (Store.TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } } /// @@ -61,35 +71,12 @@ public bool ContainsKey(string key) return Store.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) - { - return GetJoinedValue(Store, key); - } - - /// - /// 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[] values; - Store.TryGetValue(key, out values); - return values; - } /// /// /// /// - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { return Store.GetEnumerator(); } @@ -102,15 +89,5 @@ IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } - - private static string GetJoinedValue(IDictionary store, string key) - { - string[] values; - if (store.TryGetValue(key, out values)) - { - return string.Join(",", values); - } - return null; - } } } diff --git a/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs b/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs index 1dc603b5..9b0a5693 100644 --- a/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs +++ b/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs @@ -17,7 +17,7 @@ public RequestCookiesCollection() _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public string this[string key] + public StringValues this[string key] { get { return Get(key); } } @@ -88,11 +88,11 @@ public void Reparse(IList values) } } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { foreach (var pair in _dictionary) { - yield return new KeyValuePair(pair.Key, new[] { pair.Value }); + yield return new KeyValuePair(pair.Key, pair.Value); } } diff --git a/src/Microsoft.AspNet.Http/ResponseCookies.cs b/src/Microsoft.AspNet.Http/ResponseCookies.cs index b47b48d5..21a9f3e2 100644 --- a/src/Microsoft.AspNet.Http/ResponseCookies.cs +++ b/src/Microsoft.AspNet.Http/ResponseCookies.cs @@ -33,11 +33,14 @@ public ResponseCookies([NotNull] IHeaderDictionary headers) /// public void Append(string key, string value) { - Headers.AppendValues(HeaderNames.SetCookie, - new SetCookieHeaderValue( + var setCookieHeaderValue = new SetCookieHeaderValue( UrlEncoder.Default.UrlEncode(key), UrlEncoder.Default.UrlEncode(value)) - { Path = "/" }.ToString()); + { + Path = "/" + }; + + Headers[HeaderNames.SetCookie] += setCookieHeaderValue.ToString(); } /// @@ -48,17 +51,18 @@ public void Append(string key, string value) /// public void Append(string key, string value, [NotNull] CookieOptions options) { - Headers.AppendValues(HeaderNames.SetCookie, - new SetCookieHeaderValue( + var setCookieHeaderValue = new SetCookieHeaderValue( UrlEncoder.Default.UrlEncode(key), UrlEncoder.Default.UrlEncode(value)) - { - Domain = options.Domain, - Path = options.Path, - Expires = options.Expires, - Secure = options.Secure, - HttpOnly = options.HttpOnly, - }.ToString()); + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + Secure = options.Secure, + HttpOnly = options.HttpOnly, + }; + + Headers[HeaderNames.SetCookie] += setCookieHeaderValue; } /// @@ -70,15 +74,15 @@ public void Delete(string key) var encodedKeyPlusEquals = UrlEncoder.Default.UrlEncode(key) + "="; Func predicate = value => value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase); - var deleteCookies = new[] { encodedKeyPlusEquals + "; expires=Thu, 01-Jan-1970 00:00:00 GMT" }; - IList existingValues = Headers.GetValues(HeaderNames.SetCookie); - if (existingValues == null || existingValues.Count == 0) + StringValues deleteCookies = encodedKeyPlusEquals + "; expires=Thu, 01-Jan-1970 00:00:00 GMT"; + var existingValues = Headers[HeaderNames.SetCookie]; + if (StringValues.IsNullOrEmpty(existingValues)) { - Headers.SetValues(HeaderNames.SetCookie, deleteCookies); + Headers[HeaderNames.SetCookie] = deleteCookies; } else { - Headers.SetValues(HeaderNames.SetCookie, existingValues.Where(value => !predicate(value)).Concat(deleteCookies).ToArray()); + Headers[HeaderNames.SetCookie] = existingValues.Where(value => !predicate(value)).Concat(deleteCookies).ToArray(); } } @@ -111,10 +115,10 @@ public void Delete(string key, [NotNull] CookieOptions options) rejectPredicate = value => value.StartsWith(encodedKeyPlusEquals, StringComparison.OrdinalIgnoreCase); } - IList existingValues = Headers.GetValues(HeaderNames.SetCookie); - if (existingValues != null) + var existingValues = Headers[HeaderNames.SetCookie]; + if (!StringValues.IsNullOrEmpty(existingValues)) { - Headers.SetValues(HeaderNames.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + Headers[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray(); } Append(key, string.Empty, new CookieOptions diff --git a/src/Microsoft.AspNet.Owin/DictionaryStringArrayWrapper.cs b/src/Microsoft.AspNet.Owin/DictionaryStringArrayWrapper.cs new file mode 100644 index 00000000..9c3296ca --- /dev/null +++ b/src/Microsoft.AspNet.Owin/DictionaryStringArrayWrapper.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNet.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Collections; + +namespace Microsoft.AspNet.Owin +{ + internal class DictionaryStringArrayWrapper : IDictionary + { + public DictionaryStringArrayWrapper(IDictionary inner) + { + Inner = inner; + } + + public readonly IDictionary Inner; + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + string[] IDictionary.this[string key] + { + get { return Inner[key]; } + set { Inner[key] = value; } + } + + int ICollection>.Count => Inner.Count; + + bool ICollection>.IsReadOnly => Inner.IsReadOnly; + + ICollection IDictionary.Keys => Inner.Keys; + + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + + void IDictionary.Add(string key, string[] value) => Inner.Add(key, value); + + void ICollection>.Clear() => Inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach(var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + + bool IDictionary.Remove(string key) => Inner.Remove(key); + + bool IDictionary.TryGetValue(string key, out string[] value) + { + StringValues temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Owin/DictionaryStringValuesWrapper.cs b/src/Microsoft.AspNet.Owin/DictionaryStringValuesWrapper.cs new file mode 100644 index 00000000..ad033e23 --- /dev/null +++ b/src/Microsoft.AspNet.Owin/DictionaryStringValuesWrapper.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNet.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Collections; + +namespace Microsoft.AspNet.Owin +{ + internal class DictionaryStringValuesWrapper : IDictionary + { + public DictionaryStringValuesWrapper(IDictionary inner) + { + Inner = inner; + } + + public readonly IDictionary Inner; + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + StringValues IDictionary.this[string key] + { + get { return Inner[key]; } + set { Inner[key] = value; } + } + + int ICollection>.Count => Inner.Count; + + bool ICollection>.IsReadOnly => Inner.IsReadOnly; + + ICollection IDictionary.Keys => Inner.Keys; + + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + + void IDictionary.Add(string key, StringValues value) => Inner.Add(key, value); + + void ICollection>.Clear() => Inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + + bool IDictionary.Remove(string key) => Inner.Remove(key); + + bool IDictionary.TryGetValue(string key, out StringValues value) + { + string[] temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs index 5c231eb0..840da1b6 100644 --- a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs +++ b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs @@ -55,12 +55,12 @@ public OwinEnvironment(HttpContext context) { OwinConstants.RequestPath, new FeatureMap(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value)) }, { OwinConstants.RequestQueryString, new FeatureMap(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value))) }, - { OwinConstants.RequestHeaders, new FeatureMap(feature => feature.Headers, (feature, value) => feature.Headers = (IDictionary)value) }, + { OwinConstants.RequestHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeDictionaryStringValues((IDictionary)value)) }, { OwinConstants.RequestBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, { OwinConstants.ResponseStatusCode, new FeatureMap(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value)) }, { OwinConstants.ResponseReasonPhrase, new FeatureMap(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value)) }, - { OwinConstants.ResponseHeaders, new FeatureMap(feature => feature.Headers, (feature, value) => feature.Headers = (IDictionary)value) }, + { OwinConstants.ResponseHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeDictionaryStringValues((IDictionary)value)) }, { OwinConstants.ResponseBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap( feature => new Action, object>((cb, state) => { diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index 6dd44e52..dd6b8efc 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Owin { + using Http; using SendFileFunc = Func; public class OwinFeatureCollection : @@ -104,10 +105,10 @@ string IHttpRequestFeature.QueryString set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); } } - IDictionary IHttpRequestFeature.Headers + IDictionary IHttpRequestFeature.Headers { - get { return Prop>(OwinConstants.RequestHeaders); } - set { Prop(OwinConstants.RequestHeaders, value); } + get { return Utilities.MakeDictionaryStringValues(Prop>(OwinConstants.RequestHeaders)); } + set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); } } string IHttpRequestIdentifierFeature.TraceIdentifier @@ -134,10 +135,10 @@ string IHttpResponseFeature.ReasonPhrase set { Prop(OwinConstants.ResponseReasonPhrase, value); } } - IDictionary IHttpResponseFeature.Headers + IDictionary IHttpResponseFeature.Headers { - get { return Prop>(OwinConstants.ResponseHeaders); } - set { Prop(OwinConstants.ResponseHeaders, value); } + get { return Utilities.MakeDictionaryStringValues(Prop>(OwinConstants.ResponseHeaders)); } + set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); } } Stream IHttpResponseFeature.Body diff --git a/src/Microsoft.AspNet.Owin/Utilities.cs b/src/Microsoft.AspNet.Owin/Utilities.cs index fd3c6903..202b3e81 100644 --- a/src/Microsoft.AspNet.Owin/Utilities.cs +++ b/src/Microsoft.AspNet.Owin/Utilities.cs @@ -1,8 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; using System.Security.Claims; using System.Security.Principal; +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Owin { @@ -41,5 +44,25 @@ internal static ClaimsPrincipal MakeClaimsPrincipal(IPrincipal principal) } return new ClaimsPrincipal(principal); } + + internal static IDictionary MakeDictionaryStringValues(IDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringArrayWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringValuesWrapper(dictionary); + } + + internal static IDictionary MakeDictionaryStringArray(IDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringValuesWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringArrayWrapper(dictionary); + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNet.Http.Features.Tests/StringValuesTests.cs b/test/Microsoft.AspNet.Http.Features.Tests/StringValuesTests.cs new file mode 100644 index 00000000..f36b8fc6 --- /dev/null +++ b/test/Microsoft.AspNet.Http.Features.Tests/StringValuesTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Http.Features +{ + public class StringValuesTests + { + [Fact] + public void IsReadOnly_True() + { + var stringValues = new StringValues(); + Assert.True(((IList)stringValues).IsReadOnly); + Assert.Throws(() => ((IList)stringValues)[0] = string.Empty); + Assert.Throws(() => ((ICollection)stringValues).Add(string.Empty)); + Assert.Throws(() => ((IList)stringValues).Insert(0, string.Empty)); + Assert.Throws(() => ((ICollection)stringValues).Remove(string.Empty)); + Assert.Throws(() => ((IList)stringValues).RemoveAt(0)); + Assert.Throws(() => ((ICollection)stringValues).Clear()); + } + + [Fact] + public void DefaultConstructor_ExpectedValues() + { + var stringValues = new StringValues(); + Assert.Equal(0, stringValues.Count); + Assert.Equal((string)null, stringValues); + Assert.Equal(new string[0], stringValues); + + Assert.True(StringValues.IsNullOrEmpty(stringValues)); + Assert.Throws(() => stringValues[0]); + Assert.Equal(string.Empty, stringValues.ToString()); + Assert.Equal(-1, ((IList)stringValues).IndexOf(string.Empty)); + Assert.Equal(0, stringValues.Count()); + } + + [Fact] + public void Constructor_NullStringValue_ExpectedValues() + { + var stringValues = new StringValues((string)null); + Assert.Equal(0, stringValues.Count); + Assert.Equal((string)null, stringValues); + Assert.Equal(new string[0], stringValues); + + Assert.True(StringValues.IsNullOrEmpty(stringValues)); + Assert.Throws(() => stringValues[0]); + Assert.Equal(string.Empty, stringValues.ToString()); + Assert.Equal(-1, ((IList)stringValues).IndexOf(string.Empty)); + Assert.Equal(0, stringValues.Count()); + } + + [Fact] + public void Constructor_NullStringArray_ExpectedValues() + { + var stringValues = new StringValues((string[])null); + Assert.Equal(0, stringValues.Count); + Assert.Equal((string)null, stringValues); + Assert.Equal(new string[0], stringValues); + + Assert.True(StringValues.IsNullOrEmpty(stringValues)); + Assert.Throws(() => stringValues[0]); + Assert.Equal(string.Empty, stringValues.ToString()); + Assert.Equal(-1, ((IList)stringValues).IndexOf(string.Empty)); + Assert.Equal(0, stringValues.Count()); + } + + [Fact] + public void ImplicitStringConverter_Works() + { + string nullString = null; + StringValues stringValues = nullString; + Assert.Equal(0, stringValues.Count); + Assert.Equal((string)null, stringValues); + Assert.Equal(new string[0], stringValues); + + string aString = "abc"; + stringValues = aString; + Assert.Equal(1, stringValues.Count); + Assert.Equal(aString, stringValues); + Assert.Equal(aString, stringValues[0]); + Assert.Equal(new string[] { aString }, stringValues); + } + + [Fact] + public void ImplicitStringArrayConverter_Works() + { + string[] nullStringArray = null; + StringValues stringValues = nullStringArray; + Assert.Equal(0, stringValues.Count); + Assert.Equal((string)null, stringValues); + Assert.Equal(new string[0], stringValues); + + string aString = "abc"; + string[] aStringArray = new[] { aString }; + stringValues = aStringArray; + Assert.Equal(1, stringValues.Count); + Assert.Equal(aString, stringValues); + Assert.Equal(aString, stringValues[0]); + Assert.Equal(aStringArray, stringValues); + + aString = "abc"; + string bString = "bcd"; + aStringArray = new[] { aString, bString }; + stringValues = aStringArray; + Assert.Equal(2, stringValues.Count); + Assert.Equal("abc,bcd", stringValues); + Assert.Equal(aStringArray, stringValues); + } + + [Fact] + public void PlusOperatorOverload_Works() + { + string string1 = "string1"; + string string2 = "string2"; + string[] array1 = new[] { "array1" }; + string[] array2 = new[] { "array2" }; + StringValues values1 = array1; + StringValues values2 = array2; + + Assert.Equal("string1string2", string1 + string2); + Assert.Equal(new StringValues(new string[] { "array1", "array2" }), values1 + values2); + Assert.Equal(new StringValues(new string[] { "array1", "string2" }), values1 + string2); + Assert.Equal(new StringValues(new string[] { "string1", "array2" }), string1 + values2); + Assert.Equal(new StringValues(new string[] { "array1", "array2" }), values1 + array2); + Assert.Equal(new StringValues(new string[] { "array1", "array2" }), array1 + values2); + } + } +} diff --git a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs index 81b268ba..fa2c866a 100644 --- a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs @@ -64,9 +64,9 @@ public void Host_GetsHostFromHeaders() // Arrange const string expected = "localhost:9001"; - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Host", new string[] { expected } }, + { "Host", expected }, }; var request = CreateRequest(headers); @@ -84,9 +84,9 @@ public void Host_DecodesPunyCode() // Arrange const string expected = "löcalhöst"; - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Host", new string[]{ "xn--lcalhst-90ae" } }, + { "Host", "xn--lcalhst-90ae" }, }; var request = CreateRequest(headers); @@ -104,7 +104,7 @@ public void Host_EncodesPunyCode() // Arrange const string expected = "xn--lcalhst-90ae"; - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); var request = CreateRequest(headers); @@ -149,9 +149,9 @@ public void Query_GetAndSet() Assert.Equal("value0", query1["name0"]); Assert.Equal("value1", query1["name1"]); - var query2 = new ReadableStringCollection(new Dictionary() + var query2 = new ReadableStringCollection(new Dictionary() { - { "name2", new[] { "value2" } } + { "name2", "value2" } }); request.Query = query2; @@ -164,30 +164,30 @@ public void Query_GetAndSet() public void Cookies_GetAndSet() { var request = new DefaultHttpContext().Request; - var cookieHeaders = request.Headers.GetValues("Cookie"); - Assert.Null(cookieHeaders); + var cookieHeaders = request.Headers["Cookie"]; + Assert.Equal(0, cookieHeaders.Count); var cookies0 = request.Cookies; Assert.Equal(0, cookies0.Count); - request.Headers.SetValues("Cookie", new[] { "name0=value0", "name1=value1" }); + request.Headers["Cookie"] = new[] { "name0=value0", "name1=value1" }; var cookies1 = request.Cookies; Assert.Same(cookies0, cookies1); Assert.Equal(2, cookies1.Count); Assert.Equal("value0", cookies1["name0"]); Assert.Equal("value1", cookies1["name1"]); - var cookies2 = new ReadableStringCollection(new Dictionary() + var cookies2 = new ReadableStringCollection(new Dictionary() { - { "name2", new[] { "value2" } } + { "name2", "value2" } }); request.Cookies = cookies2; Assert.Same(cookies2, request.Cookies); Assert.Equal("value2", request.Cookies["name2"]); - cookieHeaders = request.Headers.GetValues("Cookie"); + cookieHeaders = request.Headers["Cookie"]; Assert.Equal(new[] { "name2=value2" }, cookieHeaders); } - private static HttpRequest CreateRequest(IDictionary headers) + private static HttpRequest CreateRequest(IDictionary headers) { var context = new DefaultHttpContext(); context.GetFeature().Headers = headers; @@ -216,10 +216,10 @@ private static HttpRequest GetRequestWithAcceptCharsetHeader(string acceptCharse private static HttpRequest GetRequestWithHeader(string headerName, string headerValue) { - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); if (headerValue != null) { - headers.Add(headerName, new[] { headerValue }); + headers.Add(headerName, headerValue); } return CreateRequest(headers); diff --git a/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs b/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs index 79cbac89..b3e1cb3b 100644 --- a/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs @@ -13,9 +13,9 @@ public class HeaderDictionaryTests public void PropertiesAreAccessible() { var headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) + new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Header1", new[] { "Value1" } } + { "Header1", "Value1" } }); Assert.Equal(1, headers.Count); @@ -23,8 +23,7 @@ public void PropertiesAreAccessible() Assert.True(headers.ContainsKey("header1")); Assert.False(headers.ContainsKey("header2")); Assert.Equal("Value1", headers["header1"]); - Assert.Equal("Value1", headers.Get("header1")); - Assert.Equal(new[] { "Value1" }, headers.GetValues("header1")); + Assert.Equal(new[] { "Value1" }, (string[])headers["header1"]); } } } \ No newline at end of file