Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 4c3a3ea

Browse files
committed
Use pooled StringBuilder to reduce allocations when adding response cookies
- #561
1 parent 5c9f3b6 commit 4c3a3ea

File tree

6 files changed

+100
-30
lines changed

6 files changed

+100
-30
lines changed

src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
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.Text;
45
using Microsoft.AspNetCore.Http.Internal;
6+
using Microsoft.Extensions.ObjectPool;
57

68
namespace Microsoft.AspNetCore.Http.Features.Internal
79
{
810
public class ResponseCookiesFeature : IResponseCookiesFeature
911
{
1012
private FeatureReferences<IHttpResponseFeature> _features;
13+
private FeatureReferences<IServiceProvidersFeature> _services;
1114
private IResponseCookies _cookiesCollection;
1215

1316
public ResponseCookiesFeature(IFeatureCollection features)
1417
{
1518
_features = new FeatureReferences<IHttpResponseFeature>(features);
19+
_services = new FeatureReferences<IServiceProvidersFeature>(features);
1620
}
1721

18-
private IHttpResponseFeature HttpResponseFeature =>
19-
_features.Fetch(ref _features.Cache, f => null);
22+
private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, f => null);
23+
24+
private IServiceProvidersFeature ServiceProvidersFeature => _services.Fetch(ref _services.Cache, f => null);
2025

2126
public IResponseCookies Cookies
2227
{
@@ -25,8 +30,11 @@ public IResponseCookies Cookies
2530
if (_cookiesCollection == null)
2631
{
2732
var headers = HttpResponseFeature.Headers;
28-
_cookiesCollection = new ResponseCookies(headers);
33+
var serviceProvider = ServiceProvidersFeature.RequestServices;
34+
var pool = (ObjectPool<StringBuilder>)serviceProvider.GetService(typeof(ObjectPool<StringBuilder>));
35+
_cookiesCollection = new ResponseCookies(headers, pool);
2936
}
37+
3038
return _cookiesCollection;
3139
}
3240
}

src/Microsoft.AspNetCore.Http/ResponseCookies.cs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Text.Encodings.Web;
65
using System.Collections.Generic;
6+
using System.Text;
7+
using Microsoft.Extensions.ObjectPool;
78
using Microsoft.Extensions.Primitives;
89
using Microsoft.Net.Http.Headers;
910

@@ -14,18 +15,26 @@ namespace Microsoft.AspNetCore.Http.Internal
1415
/// </summary>
1516
public class ResponseCookies : IResponseCookies
1617
{
18+
private readonly ObjectPool<StringBuilder> _builderPool;
19+
1720
/// <summary>
1821
/// Create a new wrapper
1922
/// </summary>
20-
/// <param name="headers"></param>
21-
public ResponseCookies(IHeaderDictionary headers)
23+
/// <param name="headers">The <see cref="IHeaderDictionary"/> for the response.</param>
24+
/// <param name="builderPool">The <see cref="ObjectPool{T}"/> used.</param>
25+
public ResponseCookies(IHeaderDictionary headers, ObjectPool<StringBuilder> builderPool)
2226
{
2327
if (headers == null)
2428
{
2529
throw new ArgumentNullException(nameof(headers));
2630
}
31+
if (builderPool == null)
32+
{
33+
throw new ArgumentNullException(nameof(builderPool));
34+
}
2735

2836
Headers = headers;
37+
_builderPool = builderPool;
2938
}
3039

3140
private IHeaderDictionary Headers { get; set; }
@@ -38,13 +47,25 @@ public ResponseCookies(IHeaderDictionary headers)
3847
public void Append(string key, string value)
3948
{
4049
var setCookieHeaderValue = new SetCookieHeaderValue(
41-
Uri.EscapeDataString(key),
42-
Uri.EscapeDataString(value))
50+
Uri.EscapeDataString(key),
51+
Uri.EscapeDataString(value))
4352
{
4453
Path = "/"
4554
};
4655

47-
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString());
56+
string cookieValue;
57+
var stringBuilder = _builderPool.Get();
58+
try
59+
{
60+
setCookieHeaderValue.AddStringValue(stringBuilder);
61+
cookieValue = stringBuilder.ToString();
62+
}
63+
finally
64+
{
65+
_builderPool.Return(stringBuilder);
66+
}
67+
68+
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
4869
}
4970

5071
/// <summary>
@@ -61,8 +82,8 @@ public void Append(string key, string value, CookieOptions options)
6182
}
6283

6384
var setCookieHeaderValue = new SetCookieHeaderValue(
64-
Uri.EscapeDataString(key),
65-
Uri.EscapeDataString(value))
85+
Uri.EscapeDataString(key),
86+
Uri.EscapeDataString(value))
6687
{
6788
Domain = options.Domain,
6889
Path = options.Path,
@@ -71,7 +92,19 @@ public void Append(string key, string value, CookieOptions options)
7192
HttpOnly = options.HttpOnly,
7293
};
7394

74-
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString());
95+
string cookieValue;
96+
var stringBuilder = _builderPool.Get();
97+
try
98+
{
99+
setCookieHeaderValue.AddStringValue(stringBuilder);
100+
cookieValue = stringBuilder.ToString();
101+
}
102+
finally
103+
{
104+
_builderPool.Return(stringBuilder);
105+
}
106+
107+
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
75108
}
76109

77110
/// <summary>
@@ -94,7 +127,7 @@ public void Delete(string key, CookieOptions options)
94127
{
95128
throw new ArgumentNullException(nameof(options));
96129
}
97-
130+
98131
var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "=";
99132
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
100133
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
@@ -130,7 +163,7 @@ public void Delete(string key, CookieOptions options)
130163
newValues.Add(values[i]);
131164
}
132165
}
133-
166+
134167
Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray());
135168
}
136169

src/Microsoft.AspNetCore.Http/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
1919
"Microsoft.AspNetCore.WebUtilities": "1.0.0-*",
20+
"Microsoft.Extensions.ObjectPool": "1.0.0-*",
2021
"Microsoft.Net.Http.Headers": "1.0.0-*"
2122
},
2223
"frameworks": {

src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,42 +90,53 @@ public string Value
9090
public override string ToString()
9191
{
9292
StringBuilder header = new StringBuilder();
93+
AddStringValue(header);
9394

94-
header.Append(_name);
95-
header.Append("=");
96-
header.Append(_value);
95+
return header.ToString();
96+
}
97+
98+
/// <summary>
99+
/// Add string representation of this <see cref="SetCookieHeaderValue"/> to given <paramref name="builder"/>.
100+
/// </summary>
101+
/// <param name="builder">
102+
/// The <see cref="StringBuilder"/> to receive the string representation of this
103+
/// <see cref="SetCookieHeaderValue"/>.
104+
/// </param>
105+
public void AddStringValue(StringBuilder builder)
106+
{
107+
builder.Append(_name);
108+
builder.Append("=");
109+
builder.Append(_value);
97110

98111
if (Expires.HasValue)
99112
{
100-
AppendSegment(header, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
113+
AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
101114
}
102115

103116
if (MaxAge.HasValue)
104117
{
105-
AppendSegment(header, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
118+
AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
106119
}
107120

108121
if (Domain != null)
109122
{
110-
AppendSegment(header, DomainToken, Domain);
123+
AppendSegment(builder, DomainToken, Domain);
111124
}
112125

113126
if (Path != null)
114127
{
115-
AppendSegment(header, PathToken, Path);
128+
AppendSegment(builder, PathToken, Path);
116129
}
117130

118131
if (Secure)
119132
{
120-
AppendSegment(header, SecureToken, null);
133+
AppendSegment(builder, SecureToken, null);
121134
}
122135

123136
if (HttpOnly)
124137
{
125-
AppendSegment(header, HttpOnlyToken, null);
138+
AppendSegment(builder, HttpOnlyToken, null);
126139
}
127-
128-
return header.ToString();
129140
}
130141

131142
private static void AppendSegment(StringBuilder builder, string name, string value)

test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
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 Xunit;
5-
using Microsoft.Net.Http.Headers;
4+
using System.Text;
65
using Microsoft.AspNetCore.Http.Internal;
6+
using Microsoft.Extensions.ObjectPool;
7+
using Microsoft.Net.Http.Headers;
8+
using Xunit;
79

810
namespace Microsoft.AspNetCore.Http.Tests
911
{
1012
public class ResponseCookiesTest
1113
{
14+
private static readonly ObjectPool<StringBuilder> _builderPool =
15+
new DefaultObjectPoolProvider().Create<StringBuilder>(new StringBuilderPooledObjectPolicy());
16+
1217
[Fact]
1318
public void DeleteCookieShouldSetDefaultPath()
1419
{
1520
var headers = new HeaderDictionary();
16-
var cookies = new ResponseCookies(headers);
21+
var cookies = new ResponseCookies(headers, _builderPool);
1722
var testcookie = "TestCookie";
1823

1924
cookies.Delete(testcookie);
@@ -29,7 +34,7 @@ public void DeleteCookieShouldSetDefaultPath()
2934
public void NoParamsDeleteRemovesCookieCreatedByAdd()
3035
{
3136
var headers = new HeaderDictionary();
32-
var cookies = new ResponseCookies(headers);
37+
var cookies = new ResponseCookies(headers, _builderPool);
3338
var testcookie = "TestCookie";
3439

3540
cookies.Append(testcookie, testcookie);
@@ -49,7 +54,7 @@ public void NoParamsDeleteRemovesCookieCreatedByAdd()
4954
public void EscapesKeyValuesBeforeSettingCookie(string key, string value, string expected)
5055
{
5156
var headers = new HeaderDictionary();
52-
var cookies = new ResponseCookies(headers);
57+
var cookies = new ResponseCookies(headers, _builderPool);
5358

5459
cookies.Append(key, value);
5560

test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text;
78
using Xunit;
89

910
namespace Microsoft.Net.Http.Headers
@@ -264,6 +265,17 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp
264265
Assert.Equal(expectedValue, input.ToString());
265266
}
266267

268+
[Theory]
269+
[MemberData(nameof(SetCookieHeaderDataSet))]
270+
public void SetCookieHeaderValue_AddStringValue(SetCookieHeaderValue input, string expectedValue)
271+
{
272+
var builder = new StringBuilder();
273+
274+
input.AddStringValue(builder);
275+
276+
Assert.Equal(expectedValue, builder.ToString());
277+
}
278+
267279
[Theory]
268280
[MemberData(nameof(SetCookieHeaderDataSet))]
269281
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)

0 commit comments

Comments
 (0)