Skip to content

Commit ec0841d

Browse files
authored
Reduce allocations for Cookies. (#31258)
1 parent 9252bbc commit ec0841d

13 files changed

+299
-203
lines changed
Lines changed: 8 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System.Diagnostics.Contracts;
54
using Microsoft.Extensions.Primitives;
65

76
namespace Microsoft.Net.Http.Headers
@@ -13,83 +12,24 @@ internal CookieHeaderParser(bool supportsMultipleValues)
1312
{
1413
}
1514

16-
public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue)
15+
public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue)
1716
{
18-
parsedValue = null;
17+
cookieValue = null;
1918

20-
// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
21-
// be added multiple times to the request/response message. E.g.
22-
// Accept: text/xml; q=1
23-
// Accept:
24-
// Accept: text/plain; q=0.2
25-
if (StringSegment.IsNullOrEmpty(value) || (index == value.Length))
26-
{
27-
return SupportsMultipleValues;
28-
}
29-
30-
var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out bool separatorFound);
31-
32-
if (separatorFound && !SupportsMultipleValues)
33-
{
34-
return false; // leading separators not allowed if we don't support multiple values.
35-
}
36-
37-
if (current == value.Length)
38-
{
39-
if (SupportsMultipleValues)
40-
{
41-
index = current;
42-
}
43-
return SupportsMultipleValues;
44-
}
45-
46-
if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result))
19+
if (!CookieHeaderParserShared.TryParseValue(value, ref index, SupportsMultipleValues, out var parsedName, out var parsedValue))
4720
{
4821
return false;
4922
}
5023

51-
current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound);
52-
53-
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
54-
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
24+
if (parsedName == null || parsedValue == null)
5525
{
56-
return false;
26+
// Successfully parsed, but no values.
27+
return true;
5728
}
5829

59-
index = current;
60-
parsedValue = result;
61-
return true;
62-
}
63-
64-
private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound)
65-
{
66-
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
67-
68-
separatorFound = false;
69-
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
70-
71-
if ((current == input.Length) || (input[current] != ',' && input[current] != ';'))
72-
{
73-
return current;
74-
}
75-
76-
// If we have a separator, skip the separator and all following whitespaces. If we support
77-
// empty values, continue until the current character is neither a separator nor a whitespace.
78-
separatorFound = true;
79-
current++; // skip delimiter.
80-
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
81-
82-
if (skipEmptyValues)
83-
{
84-
// Most headers only split on ',', but cookies primarily split on ';'
85-
while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';')))
86-
{
87-
current++; // skip delimiter.
88-
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
89-
}
90-
}
30+
cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value);
9131

92-
return current;
32+
return true;
9333
}
9434
}
9535
}

src/Http/Headers/src/CookieHeaderValue.cs

Lines changed: 1 addition & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -165,121 +165,6 @@ public static bool TryParseStrictList(IList<string>? inputs, [NotNullWhen(true)]
165165
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
166166
}
167167

168-
// name=value; name="value"
169-
internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out CookieHeaderValue? parsedValue)
170-
{
171-
Contract.Requires(offset >= 0);
172-
173-
parsedValue = null;
174-
175-
if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length))
176-
{
177-
return false;
178-
}
179-
180-
var result = new CookieHeaderValue();
181-
182-
// The caller should have already consumed any leading whitespace, commas, etc..
183-
184-
// Name=value;
185-
186-
// Name
187-
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
188-
if (itemLength == 0)
189-
{
190-
return false;
191-
}
192-
result._name = input.Subsegment(offset, itemLength);
193-
offset += itemLength;
194-
195-
// = (no spaces)
196-
if (!ReadEqualsSign(input, ref offset))
197-
{
198-
return false;
199-
}
200-
201-
// value or "quoted value"
202-
// The value may be empty
203-
result._value = GetCookieValue(input, ref offset);
204-
205-
parsedValue = result;
206-
return true;
207-
}
208-
209-
// cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE )
210-
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
211-
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
212-
internal static StringSegment GetCookieValue(StringSegment input, ref int offset)
213-
{
214-
Contract.Requires(offset >= 0);
215-
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - offset)));
216-
217-
var startIndex = offset;
218-
219-
if (offset >= input.Length)
220-
{
221-
return StringSegment.Empty;
222-
}
223-
var inQuotes = false;
224-
225-
if (input[offset] == '"')
226-
{
227-
inQuotes = true;
228-
offset++;
229-
}
230-
231-
while (offset < input.Length)
232-
{
233-
var c = input[offset];
234-
if (!IsCookieValueChar(c))
235-
{
236-
break;
237-
}
238-
239-
offset++;
240-
}
241-
242-
if (inQuotes)
243-
{
244-
if (offset == input.Length || input[offset] != '"')
245-
{
246-
// Missing final quote
247-
return StringSegment.Empty;
248-
}
249-
offset++;
250-
}
251-
252-
int length = offset - startIndex;
253-
if (offset > startIndex)
254-
{
255-
return input.Subsegment(startIndex, length);
256-
}
257-
258-
return StringSegment.Empty;
259-
}
260-
261-
private static bool ReadEqualsSign(StringSegment input, ref int offset)
262-
{
263-
// = (no spaces)
264-
if (offset >= input.Length || input[offset] != '=')
265-
{
266-
return false;
267-
}
268-
offset++;
269-
return true;
270-
}
271-
272-
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
273-
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
274-
private static bool IsCookieValueChar(char c)
275-
{
276-
if (c < 0x21 || c > 0x7E)
277-
{
278-
return false;
279-
}
280-
return !(c == '"' || c == ',' || c == ';' || c == '\\');
281-
}
282-
283168
internal static void CheckNameFormat(StringSegment name, string parameterName)
284169
{
285170
if (name == null)
@@ -301,7 +186,7 @@ internal static void CheckValueFormat(StringSegment value, string parameterName)
301186
}
302187

303188
var offset = 0;
304-
var result = GetCookieValue(value, ref offset);
189+
var result = CookieHeaderParserShared.GetCookieValue(value, ref offset);
305190
if (result.Length != value.Length)
306191
{
307192
throw new ArgumentException("Invalid cookie value: " + value, parameterName);

src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
<ItemGroup>
1515
<Reference Include="Microsoft.Extensions.Primitives" />
16+
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="CookieHeaderParserShared.cs" />
17+
<Compile Include="..\..\Shared\HttpRuleParser.cs" />
18+
<Compile Include="..\..\Shared\HttpParseResult.cs" />
1619
</ItemGroup>
1720

1821
</Project>

src/Http/Headers/src/SetCookieHeaderValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
494494

495495
// value or "quoted value"
496496
// The value may be empty
497-
result._value = CookieHeaderValue.GetCookieValue(input, ref offset);
497+
result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset);
498498

499499
// *(';' SP cookie-av)
500500
while (offset < input.Length)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using Microsoft.Extensions.Primitives;
6+
7+
namespace Microsoft.AspNetCore.Http
8+
{
9+
public class RequestCookieCollectionBenchmarks
10+
{
11+
private StringValues _cookie;
12+
13+
[IterationSetup]
14+
public void Setup()
15+
{
16+
_cookie = ".AspNetCore.Cookies=CfDJ8BAklVa9EYREk8_ipRUUYJYhRsleKr485k18s_q5XD6vcRJ-DtowUuLCwwMiY728zRZ3rVFY3DEcXDAQUOTtg1e4tkSIVmYLX38Q6mqdFFyw-8dksclDywe9vnN84cEWvfV0wP3EgOsJGHaND7kTJ47gr7Pc1tLHWOm4Pe7Q1vrT9EkcTMr1Wts3aptBl3bdOLLqjmSdgk-OI7qG7uQGz1OGdnSer6-KLUPBcfXblzs4YCjvwu3bGnM42xLGtkZNIF8izPpyqKkIf7ec6O6LEHMp4gcq86PGHCXHn5NKuNSD";
17+
}
18+
19+
[Benchmark]
20+
public void Parse_TypicalCookie()
21+
{
22+
RequestCookieCollection.Parse(_cookie);
23+
}
24+
}
25+
}

src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;

src/Http/Http/src/Features/RequestCookiesFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public IRequestCookieCollection Cookies
7575
if (_parsedValues == null || _original != current)
7676
{
7777
_original = current;
78-
_parsedValues = RequestCookieCollection.Parse(current.ToArray());
78+
_parsedValues = RequestCookieCollection.Parse(current);
7979
}
8080

8181
return _parsedValues;

src/Http/Http/src/Internal/RequestCookieCollection.cs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Diagnostics.CodeAnalysis;
8+
using Microsoft.Extensions.Primitives;
89
using Microsoft.Net.Http.Headers;
910

1011
namespace Microsoft.AspNetCore.Http
@@ -56,33 +57,25 @@ public string? this[string key]
5657
}
5758
}
5859

59-
public static RequestCookieCollection Parse(IList<string> values)
60-
=> ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled);
60+
public static RequestCookieCollection Parse(StringValues values)
61+
=> ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled);
6162

62-
internal static RequestCookieCollection ParseInternal(IList<string> values, bool enableCookieNameEncoding)
63+
internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding)
6364
{
6465
if (values.Count == 0)
6566
{
6667
return Empty;
6768
}
69+
var collection = new RequestCookieCollection(values.Count);
70+
var store = collection.Store!;
6871

69-
if (CookieHeaderValue.TryParseList(values, out var cookies))
72+
if (CookieHeaderParserShared.TryParseValues(values, store, enableCookieNameEncoding, supportsMultipleValues: true))
7073
{
71-
if (cookies.Count == 0)
74+
if (store.Count == 0)
7275
{
7376
return Empty;
7477
}
7578

76-
var collection = new RequestCookieCollection(cookies.Count);
77-
var store = collection.Store!;
78-
for (var i = 0; i < cookies.Count; i++)
79-
{
80-
var cookie = cookies[i];
81-
var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Name.Value) : cookie.Name.Value;
82-
var value = Uri.UnescapeDataString(cookie.Value.Value);
83-
store[name] = value;
84-
}
85-
8679
return collection;
8780
}
8881
return Empty;

src/Http/Http/src/Microsoft.AspNetCore.Http.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core default HTTP feature implementations.</Description>
@@ -15,6 +15,9 @@
1515
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
1616
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
1717
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="Internal\StreamCopyOperationInternal.cs" />
18+
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="Internal\CookieHeaderParserShared.cs" />
19+
<Compile Include="..\..\Shared\HttpRuleParser.cs" LinkBase="Internal" />
20+
<Compile Include="..\..\Shared\HttpParseResult.cs" LinkBase="Internal" />
1821
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
1922
</ItemGroup>
2023

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
using System.Runtime.CompilerServices;
22

33
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
4+
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

0 commit comments

Comments
 (0)