From 963f086eb03c11dc7f5d9e27f0f1ed635c3be0c1 Mon Sep 17 00:00:00 2001 From: Louis DeJardin Date: Wed, 8 Jul 2015 14:45:14 -0700 Subject: [PATCH] Prototypeing a fast header dictionary Conflicts: src/Microsoft.AspNet.Server.Kestrel/project.json --- KestrelHttpServer.sln | 6 + .../KnownHeaders.cs | 305 ++++++++++++++++++ ....AspNet.Server.Kestrel.GeneratedCode.xproj | 20 ++ .../project.json | 28 ++ .../Http/FrameRequestHeaders.cs | 132 ++++++++ .../compiler/preprocess/KnownHeaders.cs | 11 + .../project.json | 3 +- .../FrameRequestHeadersTests.cs | 238 ++++++++++++++ 8 files changed, 742 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/KnownHeaders.cs create mode 100644 src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj create mode 100644 src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/project.json create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestHeaders.cs create mode 100644 src/Microsoft.AspNet.Server.Kestrel/compiler/preprocess/KnownHeaders.cs create mode 100644 test/Microsoft.AspNet.Server.KestrelTests/FrameRequestHeadersTests.cs diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index d2ebe3ed3d..207aa84895 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -26,6 +26,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LargeResponseApp", "samples\LargeResponseApp\LargeResponseApp.xproj", "{B35D4D31-E74C-4646-8A11-7A7A40F0021E}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Server.Kestrel.GeneratedCode", "src\Microsoft.AspNet.Server.Kestrel.GeneratedCode\Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj", "{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,6 +54,10 @@ Global {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.Build.0 = Release|Any CPU + {BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,5 +68,6 @@ Global {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} {30B7617E-58EF-4382-B3EA-5B2E718CF1A6} = {2D5D5227-4DBD-499A-96B1-76A36B03B750} {B35D4D31-E74C-4646-8A11-7A7A40F0021E} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} + {BD2D4D29-1BD9-40D0-BB31-337D5416B63C} = {2D5D5227-4DBD-499A-96B1-76A36B03B750} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/KnownHeaders.cs b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/KnownHeaders.cs new file mode 100644 index 0000000000..61cb71178d --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/KnownHeaders.cs @@ -0,0 +1,305 @@ +using Microsoft.Framework.Runtime.Roslyn; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Server.Kestrel.GeneratedCode +{ + // This project can output the Class library as a NuGet Package. + // To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build". + public class KnownHeaders : ICompileModule + { + string Each(IEnumerable values, Func formatter) + { + return values.Select(formatter).Aggregate((a, b) => a + b + "\r\n"); + } + + class KnownHeader + { + public string Name { get; set; } + public int Index { get; set; } + public string Identifier => Name.Replace("-", ""); + public string TestBit() => $"((_bits & ({1L << Index}L)) != 0)"; + public string SetBit() => $"_bits |= {1L << Index}L"; + public string ClearBit() => $"_bits &= ~{(1L << Index)}L"; + } + + public virtual void BeforeCompile(BeforeCompileContext context) + { + Console.WriteLine("I like pie"); + + var commonHeaders = new[] + { + "Cache-Control", + "Connection", + "Date", + "Keep-Alive", + "Pragma", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Via", + "Warning", + "Allow", + "Content-Length", + "Content-Type", + "Content-Encoding", + "Content-Language", + "Content-Location", + "Content-MD5", + "Content-Range", + "Expires", + "Last-Modified" + }; + var requestHeaders = commonHeaders.Concat(new[] + { + "Accept", + "Accept-Charset", + "Accept-Encoding", + "Accept-Language", + "Authorization", + "Cookie", + "Expect", + "From", + "Host", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", + "Max-Forwards", + "Proxy-Authorization", + "Referer", + "Range", + "TE", + "Translage", + "User-Agent", + }).Select((header, index) => new KnownHeader + { + Name = header, + Index = index + }); + + var responseHeaders = commonHeaders.Concat(new[] + { + "Accept-Ranges", + "Age", + "ETag", + "Location", + "Proxy-Autheticate", + "Retry-After", + "Server", + "Set-Cookie", + "Vary", + "WWW-Authenticate", + }).Select((header, index) => new KnownHeader + { + Name = header, + Index = index + }); + + var loops = new[] + { + new + { + Headers = requestHeaders, + HeadersByLength = requestHeaders.GroupBy(x => x.Name.Length), + ClassName = "FrameRequestHeaders" + }, + new + { + Headers = responseHeaders, + HeadersByLength = responseHeaders.GroupBy(x => x.Name.Length), + ClassName = "FrameResponseHeaders" + } + }; + + var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText($@" +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Server.Kestrel.Http +{{{Each(loops, loop => $@" + public partial class {loop.ClassName} + {{ + long _bits = 0; + {Each(loop.Headers, header => "string[] _" + header.Identifier + ";")} + + protected override int GetCountFast() + {{ + var count = Unknown.Count; + {Each(loop.Headers, header => $@" + if ({header.TestBit()}) + {{ + ++count; + }} + ")} + return count; + }} + + protected override string[] GetValueFast(string key) + {{ + switch(key.Length) + {{{Each(loop.HeadersByLength, byLength => $@" + case {byLength.Key}: + {{{Each(byLength, header => $@" + if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}"")) + {{ + if ({header.TestBit()}) + {{ + return _{header.Identifier}; + }} + else + {{ + throw new System.Collections.Generic.KeyNotFoundException(); + }} + }} + ")}}} + break; + ")}}} + return Unknown[key]; + }} + + protected override bool TryGetValueFast(string key, out string[] value) + {{ + switch(key.Length) + {{{Each(loop.HeadersByLength, byLength => $@" + case {byLength.Key}: + {{{Each(byLength, header => $@" + if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}"")) + {{ + if ({header.TestBit()}) + {{ + value = _{header.Identifier}; + return true; + }} + else + {{ + value = null; + return false; + }} + }} + ")}}} + break; + ")}}} + return Unknown.TryGetValue(key, out value); + }} + + protected override void SetValueFast(string key, string[] value) + {{ + switch(key.Length) + {{{Each(loop.HeadersByLength, byLength => $@" + case {byLength.Key}: + {{{Each(byLength, header => $@" + if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}"")) + {{ + {header.SetBit()}; + _{header.Identifier} = value; + return; + }} + ")}}} + break; + ")}}} + Unknown[key] = value; + }} + + protected override void AddValueFast(string key, string[] value) + {{ + switch(key.Length) + {{{Each(loop.HeadersByLength, byLength => $@" + case {byLength.Key}: + {{{Each(byLength, header => $@" + if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}"")) + {{ + if ({header.TestBit()}) + {{ + throw new ArgumentException(""An item with the same key has already been added.""); + }} + {header.SetBit()}; + _{header.Identifier} = value; + return; + }} + ")}}} + break; + ")}}} + Unknown.Add(key, value); + }} + + protected override bool RemoveFast(string key) + {{ + switch(key.Length) + {{{Each(loop.HeadersByLength, byLength => $@" + case {byLength.Key}: + {{{Each(byLength, header => $@" + if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}"")) + {{ + if ({header.TestBit()}) + {{ + {header.ClearBit()}; + return true; + }} + else + {{ + return false; + }} + }} + ")}}} + break; + ")}}} + return Unknown.Remove(key); + }} + + protected override void ClearFast() + {{ + _bits = 0; + Unknown.Clear(); + }} + + protected override void CopyToFast(KeyValuePair[] array, int arrayIndex) + {{ + if (arrayIndex < 0) + {{ + throw new ArgumentException(); + }} + + {Each(loop.Headers, header => $@" + if ({header.TestBit()}) + {{ + if (arrayIndex == array.Length) + {{ + throw new ArgumentException(); + }} + + array[arrayIndex] = new KeyValuePair(""{header.Name}"", _{header.Identifier}); + ++arrayIndex; + }} + ")} + ((ICollection>)Unknown).CopyTo(array, arrayIndex); + }} + + protected override IEnumerable> EnumerateFast() + {{ + {Each(loop.Headers, header => $@" + if ({header.TestBit()}) + {{ + yield return new KeyValuePair(""{header.Name}"", _{header.Identifier}); + }} + ")} + foreach(var kv in Unknown) + {{ + yield return kv; + }} + }} + }} +")}}} +"); + + context.Compilation = context.Compilation.AddSyntaxTrees(syntaxTree); + } + + public virtual void AfterCompile(AfterCompileContext context) + { + } + } +} diff --git a/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj new file mode 100644 index 0000000000..12a75e7f62 --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + bd2d4d29-1bd9-40d0-bb31-337d5416b63c + Microsoft.AspNet.Server.Kestrel.GeneratedCode + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/project.json b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/project.json new file mode 100644 index 0000000000..a9fe1fd9df --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel.GeneratedCode/project.json @@ -0,0 +1,28 @@ +{ + "version": "1.0.0-*", + "description": "", + "authors": [ "" ], + "tags": [ "" ], + "projectUrl": "", + "licenseUrl": "", + + "dependencies": { + "Microsoft.Framework.Runtime.Roslyn": "1.0.0-beta6-*" + }, + + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Runtime": "4.0.10.0" + } + }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.10-beta-*", + "System.Linq": "4.0.0-beta-*", + "System.Threading": "4.0.10-beta-*", + "Microsoft.CSharp": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestHeaders.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestHeaders.cs new file mode 100644 index 0000000000..6d6a45683b --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestHeaders.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Server.Kestrel.Http +{ + public partial class FrameRequestHeaders : FrameHeaders + { + } + + public partial class FrameResponseHeaders : FrameHeaders + { + } + + public abstract class FrameHeaders : IDictionary + { + protected Dictionary Unknown = new Dictionary(StringComparer.OrdinalIgnoreCase); + + protected virtual int GetCountFast() + { throw new NotImplementedException(); } + + protected virtual string[] GetValueFast(string key) + { throw new NotImplementedException(); } + + protected virtual bool TryGetValueFast(string key, out string[] value) + { throw new NotImplementedException(); } + + protected virtual void SetValueFast(string key, string[] value) + { throw new NotImplementedException(); } + + protected virtual void AddValueFast(string key, string[] value) + { throw new NotImplementedException(); } + + protected virtual bool RemoveFast(string key) + { throw new NotImplementedException(); } + + protected virtual void ClearFast() + { throw new NotImplementedException(); } + + protected virtual void CopyToFast(KeyValuePair[] array, int arrayIndex) + { throw new NotImplementedException(); } + + protected virtual IEnumerable> EnumerateFast() + { throw new NotImplementedException(); } + + + string[] IDictionary.this[string key] + { + get + { + return GetValueFast(key); + } + + set + { + SetValueFast(key, value); + } + } + + int ICollection>.Count => GetCountFast(); + + bool ICollection>.IsReadOnly => false; + + ICollection IDictionary.Keys => EnumerateFast().Select(x => x.Key).ToList(); + + ICollection IDictionary.Values => EnumerateFast().Select(x => x.Value).ToList(); + + void ICollection>.Add(KeyValuePair item) + { + AddValueFast(item.Key, item.Value); + } + + void IDictionary.Add(string key, string[] value) + { + AddValueFast(key, value); + } + + void ICollection>.Clear() + { + ClearFast(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + string[] value; + return + TryGetValueFast(item.Key, out value) && + object.Equals(value, item.Value); + } + + bool IDictionary.ContainsKey(string key) + { + string[] value; + return TryGetValueFast(key, out value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + CopyToFast(array, arrayIndex); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return EnumerateFast().GetEnumerator(); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return EnumerateFast().GetEnumerator(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + string[] value; + return + TryGetValueFast(item.Key, out value) && + object.Equals(value, item.Value) && + RemoveFast(item.Key); + } + + bool IDictionary.Remove(string key) + { + return RemoveFast(key); + } + + bool IDictionary.TryGetValue(string key, out string[] value) + { + return TryGetValueFast(key, out value); + } + } +} diff --git a/src/Microsoft.AspNet.Server.Kestrel/compiler/preprocess/KnownHeaders.cs b/src/Microsoft.AspNet.Server.Kestrel/compiler/preprocess/KnownHeaders.cs new file mode 100644 index 0000000000..8ef4e55532 --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel/compiler/preprocess/KnownHeaders.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Server.Kestrel +{ + public class KnownHeaders : Microsoft.AspNet.Server.Kestrel.GeneratedCode.KnownHeaders + { + } +} diff --git a/src/Microsoft.AspNet.Server.Kestrel/project.json b/src/Microsoft.AspNet.Server.Kestrel/project.json index cbcb1444df..93f24a85d1 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/project.json +++ b/src/Microsoft.AspNet.Server.Kestrel/project.json @@ -6,7 +6,8 @@ "url": "git://github.com/aspnet/kestrelhttpserver" }, "dependencies": { - "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta7-*" + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta7-*", + "Microsoft.AspNet.Server.Kestrel.GeneratedCode": { "version": "1.0.0-*", "type": "build" } }, "frameworks": { "dnx451": { }, diff --git a/test/Microsoft.AspNet.Server.KestrelTests/FrameRequestHeadersTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/FrameRequestHeadersTests.cs new file mode 100644 index 0000000000..f69b1c0746 --- /dev/null +++ b/test/Microsoft.AspNet.Server.KestrelTests/FrameRequestHeadersTests.cs @@ -0,0 +1,238 @@ +using Microsoft.AspNet.Server.Kestrel.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Server.KestrelTests +{ + public class FrameRequestHeadersTests + { + [Fact] + public void InitialDictionaryIsEmpty() + { + IDictionary headers = new FrameRequestHeaders(); + + Assert.Equal(0, headers.Count); + Assert.False(headers.IsReadOnly); + } + + [Fact] + public void SettingUnknownHeadersWorks() + { + IDictionary headers = new FrameRequestHeaders(); + + headers["custom"] = new[] { "value" }; + + Assert.NotNull(headers["custom"]); + Assert.Equal(1, headers["custom"].Length); + Assert.Equal("value", headers["custom"][0]); + } + + [Fact] + public void SettingKnownHeadersWorks() + { + IDictionary headers = new FrameRequestHeaders(); + + headers["host"] = new[] { "value" }; + + Assert.NotNull(headers["host"]); + Assert.Equal(1, headers["host"].Length); + Assert.Equal("value", headers["host"][0]); + } + + [Fact] + public void KnownAndCustomHeaderCountAddedTogether() + { + IDictionary headers = new FrameRequestHeaders(); + + headers["host"] = new[] { "value" }; + headers["custom"] = new[] { "value" }; + + Assert.Equal(2, headers.Count); + } + + [Fact] + public void TryGetValueWorksForKnownAndUnknownHeaders() + { + IDictionary headers = new FrameRequestHeaders(); + + string[] value; + Assert.False(headers.TryGetValue("host", out value)); + Assert.False(headers.TryGetValue("custom", out value)); + + headers["host"] = new[] { "value" }; + Assert.True(headers.TryGetValue("host", out value)); + Assert.False(headers.TryGetValue("custom", out value)); + + headers["custom"] = new[] { "value" }; + Assert.True(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + } + + [Fact] + public void SameExceptionThrownForMissingKey() + { + IDictionary headers = new FrameRequestHeaders(); + + Assert.Throws(() => headers["custom"]); + Assert.Throws(() => headers["host"]); + } + + [Fact] + public void EntriesCanBeEnumerated() + { + IDictionary headers = new FrameRequestHeaders(); + var v1 = new[] { "localhost" }; + var v2 = new[] { "value" }; + headers["host"] = v1; + headers["custom"] = v2; + + Assert.Equal( + new[] { + new KeyValuePair("Host", v1), + new KeyValuePair("custom", v2), + }, + headers); + } + + [Fact] + public void KeysAndValuesCanBeEnumerated() + { + IDictionary headers = new FrameRequestHeaders(); + var v1 = new[] { "localhost" }; + var v2 = new[] { "value" }; + headers["host"] = v1; + headers["custom"] = v2; + + Assert.Equal( + new[] { "Host", "custom" }, + headers.Keys); + + Assert.Equal( + new[] { v1, v2 }, + headers.Values); + } + + + [Fact] + public void ContainsAndContainsKeyWork() + { + IDictionary headers = new FrameRequestHeaders(); + var kv1 = new KeyValuePair("host", new[] { "localhost" }); + var kv2 = new KeyValuePair("custom", new[] { "value" }); + var kv1b = new KeyValuePair("host", new[] { "localhost" }); + var kv2b = new KeyValuePair("custom", new[] { "value" }); + + Assert.False(headers.ContainsKey("host")); + Assert.False(headers.ContainsKey("custom")); + Assert.False(headers.Contains(kv1)); + Assert.False(headers.Contains(kv2)); + + headers["host"] = kv1.Value; + Assert.True(headers.ContainsKey("host")); + Assert.False(headers.ContainsKey("custom")); + Assert.True(headers.Contains(kv1)); + Assert.False(headers.Contains(kv2)); + Assert.False(headers.Contains(kv1b)); + Assert.False(headers.Contains(kv2b)); + + headers["custom"] = kv2.Value; + Assert.True(headers.ContainsKey("host")); + Assert.True(headers.ContainsKey("custom")); + Assert.True(headers.Contains(kv1)); + Assert.True(headers.Contains(kv2)); + Assert.False(headers.Contains(kv1b)); + Assert.False(headers.Contains(kv2b)); + } + + [Fact] + public void AddWorksLikeSetAndThrowsIfKeyExists() + { + IDictionary headers = new FrameRequestHeaders(); + + string[] value; + Assert.False(headers.TryGetValue("host", out value)); + Assert.False(headers.TryGetValue("custom", out value)); + + headers.Add("host", new[] { "localhost" }); + headers.Add("custom", new[] { "value" }); + Assert.True(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + + Assert.Throws(() => headers.Add("host", new[] { "localhost" })); + Assert.Throws(() => headers.Add("custom", new[] { "value" })); + Assert.True(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + } + + [Fact] + public void ClearRemovesAllHeaders() + { + IDictionary headers = new FrameRequestHeaders(); + headers.Add("host", new[] { "localhost" }); + headers.Add("custom", new[] { "value" }); + + string[] value; + Assert.Equal(2, headers.Count); + Assert.True(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + + headers.Clear(); + + Assert.Equal(0, headers.Count); + Assert.False(headers.TryGetValue("host", out value)); + Assert.False(headers.TryGetValue("custom", out value)); + } + + [Fact] + public void RemoveTakesHeadersOutOfDictionary() + { + IDictionary headers = new FrameRequestHeaders(); + headers.Add("host", new[] { "localhost" }); + headers.Add("custom", new[] { "value" }); + + string[] value; + Assert.Equal(2, headers.Count); + Assert.True(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + + Assert.True(headers.Remove("host")); + Assert.False(headers.Remove("host")); + + Assert.Equal(1, headers.Count); + Assert.False(headers.TryGetValue("host", out value)); + Assert.True(headers.TryGetValue("custom", out value)); + + Assert.True(headers.Remove("custom")); + Assert.False(headers.Remove("custom")); + + Assert.Equal(0, headers.Count); + Assert.False(headers.TryGetValue("host", out value)); + Assert.False(headers.TryGetValue("custom", out value)); + } + + [Fact] + public void CopyToMovesDataIntoArray() + { + IDictionary headers = new FrameRequestHeaders(); + headers.Add("host", new[] { "localhost" }); + headers.Add("custom", new[] { "value" }); + + var entries = new KeyValuePair[4]; + headers.CopyTo(entries, 1); + + Assert.Null(entries[0].Key); + Assert.Null(entries[0].Value); + + Assert.Equal("Host", entries[1].Key); + Assert.NotNull(entries[1].Value); + + Assert.Equal("custom", entries[2].Key); + Assert.NotNull(entries[2].Value); + + Assert.Null(entries[3].Key); + Assert.Null(entries[3].Value); + } + } +}