diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index 872a0e3a..36488a5d 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22823.1 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5A15F1C-885A-452A-A731-B0173DDBD913}" EndProject @@ -43,6 +43,14 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.WebEnco EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Html.Abstractions", "src\Microsoft.AspNet.Html.Abstractions\Microsoft.AspNet.Html.Abstractions.xproj", "{68A28E4A-3ADE-4187-9625-4FF185887CB3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{982F09D8-621E-4872-BA7B-BBDEA47D1EFD}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleApp", "samples\SampleApp\SampleApp.xproj", "{1D0764B4-1DEB-4232-A714-D4B7E846918A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Primitives", "src\Microsoft.AspNet.Primitives\Microsoft.AspNet.Primitives.xproj", "{E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Primitives.Tests", "test\Microsoft.AspNet.Primitives.Tests\Microsoft.AspNet.Primitives.Tests.xproj", "{61F72E92-B3AE-4A10-B838-44F80AED40AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -245,6 +253,42 @@ Global {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|x86.ActiveCfg = Release|Any CPU {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|x86.Build.0 = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Debug|x86.Build.0 = Debug|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|Any CPU.Build.0 = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|x86.ActiveCfg = Release|Any CPU + {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|x86.Build.0 = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Debug|x86.Build.0 = Debug|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|x86.ActiveCfg = Release|Any CPU + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A}.Release|x86.Build.0 = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Debug|x86.Build.0 = Debug|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|Any CPU.Build.0 = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|x86.ActiveCfg = Release|Any CPU + {61F72E92-B3AE-4A10-B838-44F80AED40AE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -268,5 +312,8 @@ Global {7AE2731D-43CD-4CF8-850A-4914DE2CE930} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} {BE9112CB-D87D-4080-9CC3-24492D49CBE6} = {A5A15F1C-885A-452A-A731-B0173DDBD913} {68A28E4A-3ADE-4187-9625-4FF185887CB3} = {A5A15F1C-885A-452A-A731-B0173DDBD913} + {1D0764B4-1DEB-4232-A714-D4B7E846918A} = {982F09D8-621E-4872-BA7B-BBDEA47D1EFD} + {E5FACCD4-6327-43AA-80A9-AE6F4A3BFE6A} = {A5A15F1C-885A-452A-A731-B0173DDBD913} + {61F72E92-B3AE-4A10-B838-44F80AED40AE} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} EndGlobalSection EndGlobal diff --git a/samples/SampleApp/Program.cs b/samples/SampleApp/Program.cs new file mode 100644 index 00000000..295aee93 --- /dev/null +++ b/samples/SampleApp/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; +using Microsoft.AspNet.Primitives; + +namespace SampleApp +{ + public class Program + { + public void Main(string[] args) + { + for (int i = 0; i < 10; i++) + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + string myString; + string[] myArray; + StringValues myValues; + for (int j = 0; j < 100000000; j++) + { + myString = new string('a', 40); + myArray = new[] { myString }; + // myValues = new StringValues(myString); + myValues = new StringValues(myArray); + } + timer.Stop(); + Console.WriteLine(timer.Elapsed + ", " + Environment.WorkingSet); + } + } + } +} diff --git a/samples/SampleApp/SampleApp.xproj b/samples/SampleApp/SampleApp.xproj new file mode 100644 index 00000000..dcc1bdf2 --- /dev/null +++ b/samples/SampleApp/SampleApp.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 1d0764b4-1deb-4232-a714-d4b7e846918a + SampleApp + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/samples/SampleApp/project.json b/samples/SampleApp/project.json new file mode 100644 index 00000000..b3b5f51e --- /dev/null +++ b/samples/SampleApp/project.json @@ -0,0 +1,15 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*" + }, + + "commands": { + "SampleApp": "SampleApp" + }, + + "frameworks": { + "dnx451": { } + } +} diff --git a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs index b10101cb..56c9f49c 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs @@ -2,29 +2,30 @@ // 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; +using Microsoft.AspNet.Primitives; 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 + // This property is duplicated to resolve an ambiguity between IReadableStringCollection.Count and IDictionary.Count /// /// Gets the number of elements contained in the collection. /// new int Count { get; } - // This property is duplicated to resolve an ambiguity between IReadableStringCollection.Keys and IDictionary.Keys + // This property is duplicated to resolve an ambiguity between IReadableStringCollection.Keys and IDictionary.Keys /// /// Gets a collection containing the keys. /// @@ -36,21 +37,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 +53,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..36b87e61 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IReadableStringCollection.cs @@ -2,22 +2,22 @@ // 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; +using Microsoft.AspNet.Primitives; 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 +35,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.Abstractions/QueryString.cs b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs index 8717638a..182dcb17 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; @@ -143,7 +144,7 @@ public static QueryString Create(IEnumerable> param /// /// /// The resulting QueryString - public static QueryString Create(IEnumerable> parameters) + public static QueryString Create(IEnumerable> parameters) { var builder = new StringBuilder(); bool first = true; diff --git a/src/Microsoft.AspNet.Http.Abstractions/WebSocketManager.cs b/src/Microsoft.AspNet.Http.Abstractions/WebSocketManager.cs index b1dd679e..06b2b043 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/WebSocketManager.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/WebSocketManager.cs @@ -1,9 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Http { @@ -11,7 +11,7 @@ public abstract class WebSocketManager { public abstract bool IsWebSocketRequest { get; } - public abstract IList WebSocketRequestedProtocols { get; } + public abstract StringValues WebSocketRequestedProtocols { get; } public virtual Task AcceptWebSocketAsync() { diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs index 69f94e98..34775c9a 100644 --- a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Microsoft.AspNet.Http.Headers; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; @@ -50,7 +51,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 +99,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 +113,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 +159,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..1bc846a7 100644 --- a/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http.Features/IHttpRequestFeature.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Http.Features { @@ -14,7 +15,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..5f32e338 100644 --- a/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http.Features/IHttpResponseFeature.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Http.Features { @@ -12,7 +13,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/project.json b/src/Microsoft.AspNet.Http.Features/project.json index 3af9685e..46f2b789 100644 --- a/src/Microsoft.AspNet.Http.Features/project.json +++ b/src/Microsoft.AspNet.Http.Features/project.json @@ -6,7 +6,11 @@ "url": "git://github.com/aspnet/httpabstractions" }, "dependencies": { - "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } + "Microsoft.AspNet.Primitives": "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/DefaultWebSocketManager.cs b/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs index 7a8f87d1..676c998b 100644 --- a/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs +++ b/src/Microsoft.AspNet.Http/DefaultWebSocketManager.cs @@ -6,14 +6,13 @@ using System.Net.WebSockets; using System.Threading.Tasks; using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Http.Internal { public class DefaultWebSocketManager : WebSocketManager { - private static IList EmptyList = new List(); - private IFeatureCollection _features; private FeatureReference _request = FeatureReference.Default; private FeatureReference _webSockets = FeatureReference.Default; @@ -41,12 +40,11 @@ public override bool IsWebSocketRequest } } - public override IList WebSocketRequestedProtocols + public override StringValues WebSocketRequestedProtocols { get { - return ParsingHelpers.GetHeaderUnmodified(HttpRequestFeature.Headers, - HeaderNames.WebSocketSubProtocols) ?? EmptyList; + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); } } diff --git a/src/Microsoft.AspNet.Http/Features/FormFeature.cs b/src/Microsoft.AspNet.Http/Features/FormFeature.cs index 593cd274..2df99df8 100644 --- a/src/Microsoft.AspNet.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/FormFeature.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Primitives; using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; @@ -87,7 +89,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT _request.EnableRewind(); - IDictionary formFields = null; + IDictionary formFields = null; var files = new FormFileCollection(); // Some of these code paths use StreamReader which does not support cancellation tokens. @@ -102,7 +104,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT } else if (HasMultipartFormContentType(contentType)) { - var formAccumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var formAccumulator = new KeyValueAccumulator(); var boundary = GetBoundary(contentType); var multipartReader = new MultipartReader(boundary, _request.Body); @@ -111,7 +113,7 @@ public async Task ReadFormAsync(CancellationToken cancellationT { var headers = new HeaderDictionary(section.Headers); 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 +133,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 +143,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); diff --git a/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs b/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs index bce75709..aa4cc4bb 100644 --- a/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/HttpRequestFeature.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Http.Features.Internal { @@ -11,7 +12,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 +28,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..15114b29 100644 --- a/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/HttpResponseFeature.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Http.Features.Internal { @@ -13,7 +14,7 @@ public class HttpResponseFeature : IHttpResponseFeature public HttpResponseFeature() { StatusCode = 200; - Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); Body = Stream.Null; } @@ -21,7 +22,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..d0f7f18a 100644 --- a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Primitives; using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.Internal; @@ -13,17 +15,18 @@ 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 _parsedValues; + + public QueryFeature([NotNull] IDictionary query) + : this(new ReadableStringCollection(query)) { } public QueryFeature([NotNull] IReadableStringCollection query) { - _query = query; + _parsedValues = query; } public QueryFeature([NotNull] IFeatureCollection features) @@ -37,24 +40,32 @@ public IReadableStringCollection Query { if (_features == null) { - return _query; + return _parsedValues ?? ReadableStringCollection.Empty; } - var queryString = _request.Fetch(_features).QueryString; - if (_query == null || !string.Equals(_queryString, queryString, StringComparison.Ordinal)) + var current = _request.Fetch(_features).QueryString; + if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) { - _queryString = queryString; - _query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString)); + _original = current; + _parsedValues = new ReadableStringCollection(QueryHelpers.ParseQuery(current)); } - return _query; + return _parsedValues; } set { - _query = value; + _parsedValues = value; 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(_parsedValues).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..c3921e3e 100644 --- a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; @@ -14,18 +15,18 @@ 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 IReadableStringCollection _parsedValues; + + public RequestCookiesFeature([NotNull] IDictionary cookies) + : this(new ReadableStringCollection(cookies)) { } public RequestCookiesFeature([NotNull] IReadableStringCollection cookies) { - _cookies = cookies; + _parsedValues = cookies; } public RequestCookiesFeature([NotNull] IFeatureCollection features) @@ -39,46 +40,53 @@ public IReadableStringCollection Cookies { if (_features == null) { - return _cookies; + return _parsedValues ?? 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 (_parsedValues == null || !Enumerable.SequenceEqual(_original, current, StringComparer.Ordinal)) { - _cookieHeaders = values; - if (_cookiesCollection == null) + _original = current; + var collectionParser = _parsedValues as RequestCookiesCollection; + if (collectionParser == null) { - _cookiesCollection = new RequestCookiesCollection(); - _cookies = _cookiesCollection; + collectionParser = new RequestCookiesCollection(); + _parsedValues = collectionParser; } - _cookiesCollection.Reparse(values); + collectionParser.Reparse(current); } - return _cookies; + return _parsedValues; } set { - _cookies = value; - _cookieHeaders = null; - _cookiesCollection = _cookies as RequestCookiesCollection; - if (_cookies != null && _features != null) + _parsedValues = value; + _original = StringValues.Empty; + if (_features != null) { - var headers = new List(); - foreach (var pair in _cookies) + if (_parsedValues == null || _parsedValues.Count == 0) + { + _request.Fetch(_features).Headers.Remove(HeaderNames.Cookie); + } + else { - foreach (var cookieValue in pair.Value) + var headers = new List(); + foreach (var pair in _parsedValues) { - 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..9949a3c0 100644 --- a/src/Microsoft.AspNet.Http/FormCollection.cs +++ b/src/Microsoft.AspNet.Http/FormCollection.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Http.Internal @@ -11,12 +12,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..62a68137 100644 --- a/src/Microsoft.AspNet.Http/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http/HeaderDictionary.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Http.Internal @@ -15,7 +16,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 +24,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 +43,7 @@ public ICollection Keys /// /// /// - public ICollection Values + public ICollection Values { get { return Store.Values; } } @@ -69,11 +70,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 +82,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 +92,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 +103,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 +138,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 +153,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 +184,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 +193,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 +211,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 +221,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 +231,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..f8a91e65 100644 --- a/src/Microsoft.AspNet.Http/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.Http/ParsingHelpers.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; @@ -76,9 +77,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 +103,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 +135,7 @@ IEnumerator IEnumerable.GetEnumerator() internal struct Enumerator : IEnumerator { - private readonly string[] _headers; + private readonly StringValues _headers; private int _index; private string _header; @@ -149,11 +150,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 +236,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,19 +495,19 @@ 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)) { @@ -519,41 +518,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.Empty; } - 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 +588,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; - } - } - - 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 }; + headers[key] = values.Value; } } - 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,35 +618,29 @@ 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) - { - SetHeaderUnmodified(headers, key, values); - } - else - { - SetHeaderUnmodified(headers, key, existing.Concat(values)); - } + var existing = GetHeaderUnmodified(headers, key); + SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); } public static long? GetContentLength([NotNull] IHeaderDictionary headers) { const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite; long value; - string rawValue = headers.Get(HeaderNames.ContentLength); - if (!string.IsNullOrWhiteSpace(rawValue) && - long.TryParse(rawValue, styles, CultureInfo.InvariantCulture, out value)) + var rawValue = headers[HeaderNames.ContentLength]; + if (rawValue.Count == 1 && + !string.IsNullOrWhiteSpace(rawValue[0]) && + long.TryParse(rawValue[0], styles, CultureInfo.InvariantCulture, out value)) { return value; } diff --git a/src/Microsoft.AspNet.Http/ReadableStringCollection.cs b/src/Microsoft.AspNet.Http/ReadableStringCollection.cs index 7a07ea60..d6c4ce1f 100644 --- a/src/Microsoft.AspNet.Http/ReadableStringCollection.cs +++ b/src/Microsoft.AspNet.Http/ReadableStringCollection.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Http.Internal @@ -12,16 +13,18 @@ namespace Microsoft.AspNet.Http.Internal /// 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 +45,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 +72,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 +90,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..390151b4 100644 --- a/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs +++ b/src/Microsoft.AspNet.Http/RequestCookiesCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Microsoft.AspNet.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Http.Internal @@ -17,7 +18,7 @@ public RequestCookiesCollection() _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public string this[string key] + public StringValues this[string key] { get { return Get(key); } } @@ -88,11 +89,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..e83d0503 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.Collections.Generic; using System.Linq; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; using Microsoft.Net.Http.Headers; @@ -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] = StringValues.Concat(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] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString()); } /// @@ -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..ebef5bee --- /dev/null +++ b/src/Microsoft.AspNet.Owin/DictionaryStringArrayWrapper.cs @@ -0,0 +1,80 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Primitives; + +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..5b7d7659 --- /dev/null +++ b/src/Microsoft.AspNet.Owin/DictionaryStringValuesWrapper.cs @@ -0,0 +1,80 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Primitives; + +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..460faada 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -14,8 +14,10 @@ using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Owin { @@ -104,10 +106,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 +136,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..8960cb5e 100644 --- a/src/Microsoft.AspNet.Owin/Utilities.cs +++ b/src/Microsoft.AspNet.Owin/Utilities.cs @@ -1,8 +1,12 @@ // 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; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.Owin { @@ -41,5 +45,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/src/Microsoft.AspNet.Primitives/Microsoft.AspNet.Primitives.xproj b/src/Microsoft.AspNet.Primitives/Microsoft.AspNet.Primitives.xproj new file mode 100644 index 00000000..f7b40aaa --- /dev/null +++ b/src/Microsoft.AspNet.Primitives/Microsoft.AspNet.Primitives.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + e5faccd4-6327-43aa-80a9-ae6f4a3bfe6a + Microsoft.AspNet.Primitives + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.AspNet.Primitives/StringValues.cs b/src/Microsoft.AspNet.Primitives/StringValues.cs new file mode 100644 index 00000000..3312808c --- /dev/null +++ b/src/Microsoft.AspNet.Primitives/StringValues.cs @@ -0,0 +1,223 @@ +// 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; + +namespace Microsoft.AspNet.Primitives +{ + /// + /// 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 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.Primitives/project.json b/src/Microsoft.AspNet.Primitives/project.json new file mode 100644 index 00000000..4ed4ca97 --- /dev/null +++ b/src/Microsoft.AspNet.Primitives/project.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 primitive definitions.", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/httpabstractions" + }, + "dependencies": { + "Microsoft.Framework.NotNullAttribute.Sources": { + "type": "build", + "version": "1.0.0-*" + } + }, + "frameworks": { + "net451": { }, + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.11-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.WebUtilities/FormReader.cs b/src/Microsoft.AspNet.WebUtilities/FormReader.cs index 0f990043..707fe702 100644 --- a/src/Microsoft.AspNet.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNet.WebUtilities/FormReader.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.WebUtilities @@ -150,11 +151,11 @@ private async Task BufferAsync(CancellationToken cancellationToken) /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static IDictionary ReadForm(string text) + public static IDictionary ReadForm(string text) { var reader = new FormReader(text); - var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var accumulator = new KeyValueAccumulator(); var pair = reader.ReadNextPair(); while (pair.HasValue) { @@ -170,7 +171,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); } @@ -180,11 +181,11 @@ 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); - var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var accumulator = new KeyValueAccumulator(); var pair = await reader.ReadNextPairAsync(cancellationToken); while (pair.HasValue) { diff --git a/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs index 9fb4b2b6..baf04ca2 100644 --- a/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs +++ b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs @@ -1,38 +1,37 @@ // 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 Microsoft.Framework.Internal; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.WebUtilities { - public class KeyValueAccumulator + public class KeyValueAccumulator { - private Dictionary> _accumulator; - IEqualityComparer _comparer; + private Dictionary> _accumulator; - public KeyValueAccumulator([NotNull] IEqualityComparer comparer) + public KeyValueAccumulator() { - _comparer = comparer; - _accumulator = new Dictionary>(comparer); + _accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); } - public void Append(TKey key, TValue value) + public void Append(string key, string value) { - List values; + List values; if (_accumulator.TryGetValue(key, out values)) { values.Add(value); } else { - _accumulator[key] = new List(1) { value }; + _accumulator[key] = new List(1) { value }; } } - public IDictionary GetResults() + public IDictionary GetResults() { - var results = new Dictionary(_comparer); + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kv in _accumulator) { results.Add(kv.Key, kv.Value.ToArray()); diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs index 244117e3..04990ef8 100644 --- a/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs +++ b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs @@ -7,6 +7,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.WebUtilities @@ -64,10 +65,10 @@ public MultipartReader([NotNull] string boundary, [NotNull] Stream stream, int b 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(StringComparer.OrdinalIgnoreCase); + var accumulator = new KeyValueAccumulator(); var line = await _stream.ReadLineAsync(HeaderLengthLimit, cancellationToken); while (!string.IsNullOrEmpty(line)) { diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs index 5618b826..e0a35dee 100644 --- a/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs +++ b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using Microsoft.AspNet.Primitives; namespace Microsoft.AspNet.WebUtilities { @@ -12,10 +13,10 @@ public string ContentType { get { - string[] values; + StringValues values; if (Headers.TryGetValue("Content-Type", out values)) { - return string.Join(", ", values); + return values; } return null; } @@ -25,16 +26,16 @@ public string ContentDisposition { get { - string[] values; + StringValues values; if (Headers.TryGetValue("Content-Disposition", out values)) { - return string.Join(", ", values); + return values; } return null; } } - public IDictionary Headers { get; set; } + public IDictionary 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 6df0e2d7..d95d11e1 100644 --- a/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs +++ b/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.AspNet.Primitives; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; @@ -72,13 +73,13 @@ 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 IDictionary ParseQuery(string queryString) { if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?') { queryString = queryString.Substring(1); } - var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var accumulator = new KeyValueAccumulator(); int textLength = queryString.Length; int equalIndex = queryString.IndexOf('='); diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json index 496449f4..6796a73b 100644 --- a/src/Microsoft.AspNet.WebUtilities/project.json +++ b/src/Microsoft.AspNet.WebUtilities/project.json @@ -6,6 +6,7 @@ "url": "git://github.com/aspnet/httpabstractions" }, "dependencies": { + "Microsoft.AspNet.Primitives": "1.0.0-*", "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, "Microsoft.Framework.WebEncoders.Core": "1.0.0-*" }, @@ -13,7 +14,6 @@ "dnx451": { }, "dnxcore50": { "dependencies": { - "System.Collections": "4.0.11-beta-*", "System.Diagnostics.Debug": "4.0.11-beta-*", "System.IO": "4.0.11-beta-*", "System.IO.FileSystem": "4.0.1-beta-*", diff --git a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs index 81b268ba..49de3efc 100644 --- a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Primitives; using Xunit; namespace Microsoft.AspNet.Http.Internal @@ -64,9 +65,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 +85,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 +105,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 +150,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 +165,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 +217,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..30ac051f 100644 --- a/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/HeaderDictionaryTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Primitives; using Xunit; namespace Microsoft.AspNet.Http.Internal @@ -13,9 +14,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 +24,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 diff --git a/test/Microsoft.AspNet.Primitives.Tests/Microsoft.AspNet.Primitives.Tests.xproj b/test/Microsoft.AspNet.Primitives.Tests/Microsoft.AspNet.Primitives.Tests.xproj new file mode 100644 index 00000000..f1a2ba41 --- /dev/null +++ b/test/Microsoft.AspNet.Primitives.Tests/Microsoft.AspNet.Primitives.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 61f72e92-b3ae-4a10-b838-44f80aed40ae + Microsoft.AspNet.Primitives.Tests + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Primitives.Tests/StringValuesTests.cs b/test/Microsoft.AspNet.Primitives.Tests/StringValuesTests.cs new file mode 100644 index 00000000..b46f5ab6 --- /dev/null +++ b/test/Microsoft.AspNet.Primitives.Tests/StringValuesTests.cs @@ -0,0 +1,114 @@ +// 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.Primitives +{ + 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); + } + } +} diff --git a/test/Microsoft.AspNet.Primitives.Tests/project.json b/test/Microsoft.AspNet.Primitives.Tests/project.json new file mode 100644 index 00000000..88f8bbf4 --- /dev/null +++ b/test/Microsoft.AspNet.Primitives.Tests/project.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + "Microsoft.AspNet.Primitives": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +}