Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce allocations for Cookies. #31258

Merged
merged 6 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion src/Http/Headers/src/CookieHeaderParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;

Expand All @@ -13,7 +15,7 @@ internal CookieHeaderParser(bool supportsMultipleValues)
{
}

public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue)
public bool TryParseValue(StringSegment value, ref int index, out (StringSegment, StringSegment)? parsedValue)
{
parsedValue = null;

Expand Down Expand Up @@ -61,6 +63,66 @@ public override bool TryParseValue(StringSegment value, ref int index, out Cooki
return true;
}

public bool TryParseValues(StringValues values, Dictionary<string, string> store, bool enableCookieNameEncoding)
{
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
// can ignore the value.
if (values.Count == 0)
{
return false;
}
bool hasFoundValue = false;

for (var i = 0; i < values.Count; i++)
{
var value = values[i];
var index = 0;

while (!string.IsNullOrEmpty(value) && index < value.Length)
{
(StringSegment, StringSegment)? output;
if (TryParseValue(value, ref index, out output))
{
// The entry may not contain an actual value, like " , "
if (output != null)
{
var cookie = output.Value;
var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Item1.Value) : cookie.Item1.Value;
var valueString = Uri.UnescapeDataString(cookie.Item2.Value);
store[name] = valueString;
hasFoundValue = true;
}
}
else
{
// Skip the invalid values and keep trying.
index++;
}
}
}

return hasFoundValue;
}

public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue)
{
parsedValue = null;

if (!TryParseValue(value, ref index, out var stringSegments))
{
return false;
}

if (stringSegments == null)
{
return false;
}

parsedValue = new CookieHeaderValue(stringSegments.Value.Item1, stringSegments.Value.Item2);

return true;
}

private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound)
{
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
Expand Down
22 changes: 17 additions & 5 deletions src/Http/Headers/src/CookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,31 @@ public static bool TryParseStrictList(IList<string>? inputs, [NotNullWhen(true)]
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}

/// <summary>
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
/// Attempts to parse the sequence of values into a dictionary of cookies using string parsing rules.
/// </summary>
/// <param name="inputs">The values to parse.</param>
/// <param name="store"></param>
/// <param name="enableCookieNameEncoding"></param>
/// <returns></returns>
public static bool TryParseIntoDictionary(StringValues inputs, Dictionary<string, string> store, bool enableCookieNameEncoding)
{
return MultipleValueParser.TryParseValues(inputs, store, enableCookieNameEncoding);
}

// name=value; name="value"
internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out CookieHeaderValue? parsedValue)
internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out (StringSegment, StringSegment)? parsedValue)
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
{
Contract.Requires(offset >= 0);

parsedValue = null;
(StringSegment, StringSegment) result = default;

if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length))
{
return false;
}

var result = new CookieHeaderValue();

// The caller should have already consumed any leading whitespace, commas, etc..

// Name=value;
Expand All @@ -189,7 +200,8 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No
{
return false;
}
result._name = input.Subsegment(offset, itemLength);

result.Item1 = input.Subsegment(offset, itemLength);
offset += itemLength;

// = (no spaces)
Expand All @@ -200,7 +212,7 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No

// value or "quoted value"
// The value may be empty
result._value = GetCookieValue(input, ref offset);
result.Item2 = GetCookieValue(input, ref offset);

parsedValue = result;
return true;
Expand Down
1 change: 1 addition & 0 deletions src/Http/Headers/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
*REMOVED*Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag) -> void
Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool
Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void
static Microsoft.Net.Http.Headers.CookieHeaderValue.TryParseIntoDictionary(Microsoft.Extensions.Primitives.StringValues inputs, System.Collections.Generic.Dictionary<string!, string!>! store, bool enableCookieNameEncoding) -> bool
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string!
2 changes: 1 addition & 1 deletion src/Http/Http/src/Features/RequestCookiesFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public IRequestCookieCollection Cookies
if (_parsedValues == null || _original != current)
{
_original = current;
_parsedValues = RequestCookieCollection.Parse(current.ToArray());
_parsedValues = RequestCookieCollection.Parse(current);
}

return _parsedValues;
Expand Down
23 changes: 8 additions & 15 deletions src/Http/Http/src/Internal/RequestCookieCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Http
Expand Down Expand Up @@ -56,33 +57,25 @@ public string? this[string key]
}
}

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

internal static RequestCookieCollection ParseInternal(IList<string> values, bool enableCookieNameEncoding)
internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding)
{
if (values.Count == 0)
{
return Empty;
}
var collection = new RequestCookieCollection(new Dictionary<string, string>());
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
var store = collection.Store!;

if (CookieHeaderValue.TryParseList(values, out var cookies))
if (CookieHeaderValue.TryParseIntoDictionary(values, store, enableCookieNameEncoding))
{
if (cookies.Count == 0)
if (store.Count == 0)
{
return Empty;
}

var collection = new RequestCookieCollection(cookies.Count);
var store = collection.Store!;
for (var i = 0; i < cookies.Count; i++)
{
var cookie = cookies[i];
var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Name.Value) : cookie.Name.Value;
var value = Uri.UnescapeDataString(cookie.Value.Value);
store[name] = value;
}

return collection;
}
return Empty;
Expand Down