diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln
index 46b9267189..cdea46535d 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.21806.0
+VisualStudioVersion = 14.0.21916.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5A15F1C-885A-452A-A731-B0173DDBD913}"
EndProject
@@ -29,6 +29,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Exten
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Extensions.Tests", "test\Microsoft.AspNet.Http.Extensions.Tests\Microsoft.AspNet.Http.Extensions.Tests.kproj", "{AE25EF21-7F91-4B86-B73E-AF746821D339}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebUtilities", "src\Microsoft.AspNet.WebUtilities\Microsoft.AspNet.WebUtilities.kproj", "{A2FB7838-0031-4FAD-BA3E-83C30B3AF406}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebUtilities.Tests", "test\Microsoft.AspNet.WebUtilities.Tests\Microsoft.AspNet.WebUtilities.Tests.kproj", "{93C10E50-BCBB-4D8E-9492-D46E1396225B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -149,6 +153,26 @@ Global
{AE25EF21-7F91-4B86-B73E-AF746821D339}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{AE25EF21-7F91-4B86-B73E-AF746821D339}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{AE25EF21-7F91-4B86-B73E-AF746821D339}.Release|x86.ActiveCfg = Release|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406}.Release|x86.ActiveCfg = Release|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -165,5 +189,7 @@ Global
{16219571-3268-4D12-8689-12B7163DBA13} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
{CCC4363E-81E2-4058-94DD-00494E9E992A} = {A5A15F1C-885A-452A-A731-B0173DDBD913}
{AE25EF21-7F91-4B86-B73E-AF746821D339} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
+ {A2FB7838-0031-4FAD-BA3E-83C30B3AF406} = {A5A15F1C-885A-452A-A731-B0173DDBD913}
+ {93C10E50-BCBB-4D8E-9492-D46E1396225B} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj b/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj
new file mode 100644
index 0000000000..0e877575d0
--- /dev/null
+++ b/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj
@@ -0,0 +1,35 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+ Debug
+ AnyCPU
+
+
+
+ a2fb7838-0031-4fad-ba3e-83c30b3af406
+ Library
+ Microsoft.AspNet.WebUtilities
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs b/src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs
new file mode 100644
index 0000000000..a719db536d
--- /dev/null
+++ b/src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNet.Http;
+
+namespace Microsoft.AspNet.WebUtilities
+{
+ // The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } };
+ public class QueryBuilder : IEnumerable>
+ {
+ private IList> _params;
+
+ public QueryBuilder()
+ {
+ _params = new List>();
+ }
+
+ public QueryBuilder(IEnumerable> parameters)
+ {
+ _params = new List>(parameters);
+ }
+
+ public void Add(string key, string value)
+ {
+ _params.Add(new KeyValuePair(key, value));
+ }
+
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+ bool first = true;
+ for (int i = 0; i < _params.Count; i++)
+ {
+ var pair = _params[i];
+ builder.Append(first ? "?" : "&");
+ first = false;
+ builder.Append(Uri.EscapeDataString(pair.Key));
+ builder.Append("=");
+ builder.Append(Uri.EscapeDataString(pair.Value));
+ }
+
+ return builder.ToString();
+ }
+
+ public QueryString ToQueryString()
+ {
+ return new QueryString(ToString());
+ }
+
+ public override int GetHashCode()
+ {
+ return ToQueryString().GetHashCode();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ToQueryString().Equals(obj);
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return _params.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _params.GetEnumerator();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json
new file mode 100644
index 0000000000..2c127c9880
--- /dev/null
+++ b/src/Microsoft.AspNet.WebUtilities/project.json
@@ -0,0 +1,14 @@
+{
+ "version": "1.0.0-*",
+ "dependencies": {
+ "Microsoft.AspNet.Http": "1.0.0-*"
+ },
+ "frameworks": {
+ "net45": {},
+ "k10": {
+ "dependencies": {
+ "System.Runtime": "4.0.20.0"
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj b/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj
new file mode 100644
index 0000000000..a909df627d
--- /dev/null
+++ b/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj
@@ -0,0 +1,35 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+ Debug
+ AnyCPU
+
+
+
+ 93c10e50-bcbb-4d8e-9492-d46e1396225b
+ Library
+ Microsoft.AspNet.WebUtilities.Tests
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs
new file mode 100644
index 0000000000..2de60d8a46
--- /dev/null
+++ b/test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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 Xunit;
+
+namespace Microsoft.AspNet.WebUtilities
+{
+ public class QueryBuilderTests
+ {
+ [Fact]
+ public void EmptyQuery_NoQuestionMark()
+ {
+ var builder = new QueryBuilder();
+ Assert.Equal(string.Empty, builder.ToString());
+ }
+
+ [Fact]
+ public void AddSimple_NoEncoding()
+ {
+ var builder = new QueryBuilder();
+ builder.Add("key", "value");
+ Assert.Equal("?key=value", builder.ToString());
+ }
+
+ [Fact]
+ public void AddSpace_PercentEncoded()
+ {
+ var builder = new QueryBuilder();
+ builder.Add("key", "value 1");
+ Assert.Equal("?key=value%201", builder.ToString());
+ }
+
+ [Fact]
+ public void AddReservedCharacters_PercentEncoded()
+ {
+ var builder = new QueryBuilder();
+ builder.Add("key&", "value#");
+ Assert.Equal("?key%26=value%23", builder.ToString());
+ }
+
+ [Fact]
+ public void AddMultipleValues_AddedInOrder()
+ {
+ var builder = new QueryBuilder();
+ builder.Add("key1", "value1");
+ builder.Add("key2", "value2");
+ builder.Add("key3", "value3");
+ Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString());
+ }
+
+ [Fact]
+ public void AddMultipleValuesViaConstructor_AddedInOrder()
+ {
+ var builder = new QueryBuilder(new[]
+ {
+ new KeyValuePair("key1", "value1"),
+ new KeyValuePair("key2", "value2"),
+ new KeyValuePair("key3", "value3"),
+ });
+ Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString());
+ }
+
+ [Fact]
+ public void AddMultipleValuesViaInitializer_AddedInOrder()
+ {
+ var builder = new QueryBuilder()
+ {
+ { "key1", "value1" },
+ { "key2", "value2" },
+ { "key3", "value3" },
+ };
+ Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/project.json b/test/Microsoft.AspNet.WebUtilities.Tests/project.json
new file mode 100644
index 0000000000..b7217ebcea
--- /dev/null
+++ b/test/Microsoft.AspNet.WebUtilities.Tests/project.json
@@ -0,0 +1,17 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.Http": "",
+ "Microsoft.AspNet.WebUtilities": "",
+ "Xunit.KRunner": "1.0.0-*"
+ },
+ "commands": {
+ "test": "Xunit.KRunner"
+ },
+ "frameworks": {
+ "net45": {
+ "dependencies": {
+ "System.Runtime": ""
+ }
+ }
+ }
+}