Use pooled `StringBuilder` to reduce allocations when adding response cookies

- #561
- new `SetCookieHeaderValue.AppendToStringBuilder()` method; avoids per-call `StringBuilder` allocation
- `ResponseCookies` uses `ObjectPool<StringBuilder>` that `ResponseCookiesFeature` provides
 - `ResponseCookies` works fine if no `ObjectPoolProvider` is available
- `IHttpContextFactory` instance is a singleton instantiated from CI
 - make `HttpContextFactory` `ObjectPoolProvider` and `ResponseCookiesFeature`-aware
 - apply same pattern to sample `PooledHttpContextFactory`
- pool is not currently configurable; defaults are fine for response cookies
 - if we need (policy) configuration, would add an `IOptions<HttpContextFactorySettings>`

nit: Add some doc comments
This commit is contained in:
Doug Bunting 2016-03-19 16:45:26 -07:00
parent 8efc650e74
commit 80813f7c1e
11 changed files with 261 additions and 80 deletions

View File

@ -1,24 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Internal;
using Microsoft.Extensions.ObjectPool;
namespace SampleApp namespace SampleApp
{ {
public class PooledHttpContextFactory : IHttpContextFactory public class PooledHttpContextFactory : IHttpContextFactory
{ {
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Stack<PooledHttpContext> _pool = new Stack<PooledHttpContext>(); private readonly Stack<PooledHttpContext> _pool = new Stack<PooledHttpContext>();
public PooledHttpContextFactory(IHttpContextAccessor httpContextAccessor) public PooledHttpContextFactory(ObjectPoolProvider poolProvider)
: this(poolProvider, httpContextAccessor: null)
{ {
}
public PooledHttpContextFactory(ObjectPoolProvider poolProvider, IHttpContextAccessor httpContextAccessor)
{
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
_builderPool = poolProvider.CreateStringBuilderPool();
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
} }
public HttpContext Create(IFeatureCollection featureCollection) public HttpContext Create(IFeatureCollection featureCollection)
{ {
if (featureCollection == null)
{
throw new ArgumentNullException(nameof(featureCollection));
}
var responseCookiesFeature = new ResponseCookiesFeature(featureCollection, _builderPool);
featureCollection.Set<IResponseCookiesFeature>(responseCookiesFeature);
PooledHttpContext httpContext = null; PooledHttpContext httpContext = null;
lock (_pool) lock (_pool)
{ {

View File

@ -4,36 +4,39 @@
namespace Microsoft.AspNetCore.Http namespace Microsoft.AspNetCore.Http
{ {
/// <summary> /// <summary>
/// A wrapper for the response Set-Cookie header /// A wrapper for the response Set-Cookie header.
/// </summary> /// </summary>
public interface IResponseCookies public interface IResponseCookies
{ {
/// <summary> /// <summary>
/// Add a new cookie and value /// Add a new cookie and value.
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key">Name of the new cookie.</param>
/// <param name="value"></param> /// <param name="value">Value of the new cookie.</param>
void Append(string key, string value); void Append(string key, string value);
/// <summary> /// <summary>
/// Add a new cookie /// Add a new cookie.
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key">Name of the new cookie.</param>
/// <param name="value"></param> /// <param name="value">Value of the new cookie.</param>
/// <param name="options"></param> /// <param name="options"><see cref="CookieOptions"/> included in the new cookie setting.</param>
void Append(string key, string value, CookieOptions options); void Append(string key, string value, CookieOptions options);
/// <summary> /// <summary>
/// Sets an expired cookie /// Sets an expired cookie.
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key">Name of the cookie to expire.</param>
void Delete(string key); void Delete(string key);
/// <summary> /// <summary>
/// Sets an expired cookie /// Sets an expired cookie.
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key">Name of the cookie to expire.</param>
/// <param name="options"></param> /// <param name="options">
/// <see cref="CookieOptions"/> used to discriminate the particular cookie to expire. The
/// <see cref="CookieOptions.Domain"/> and <see cref="CookieOptions.Path"/> values are especially important.
/// </param>
void Delete(string key, CookieOptions options); void Delete(string key, CookieOptions options);
} }
} }

View File

@ -3,8 +3,14 @@
namespace Microsoft.AspNetCore.Http.Features namespace Microsoft.AspNetCore.Http.Features
{ {
/// <summary>
/// A helper for creating the response Set-Cookie header.
/// </summary>
public interface IResponseCookiesFeature public interface IResponseCookiesFeature
{ {
/// <summary>
/// Gets the wrapper for the response Set-Cookie header.
/// </summary>
IResponseCookies Cookies { get; } IResponseCookies Cookies { get; }
} }
} }

View File

@ -1,23 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Http.Features.Internal namespace Microsoft.AspNetCore.Http.Features.Internal
{ {
/// <summary>
/// Default implementation of <see cref="IResponseCookiesFeature"/>.
/// </summary>
public class ResponseCookiesFeature : IResponseCookiesFeature public class ResponseCookiesFeature : IResponseCookiesFeature
{ {
// Object pool will be null only in test scenarios e.g. if code news up a DefaultHttpContext.
private readonly ObjectPool<StringBuilder> _builderPool;
private FeatureReferences<IHttpResponseFeature> _features; private FeatureReferences<IHttpResponseFeature> _features;
private IResponseCookies _cookiesCollection; private IResponseCookies _cookiesCollection;
/// <summary>
/// Initializes a new <see cref="ResponseCookiesFeature"/> instance.
/// </summary>
/// <param name="features">
/// <see cref="IFeatureCollection"/> containing all defined features, including this
/// <see cref="IResponseCookiesFeature"/> and the <see cref="IHttpResponseFeature"/>.
/// </param>
public ResponseCookiesFeature(IFeatureCollection features) public ResponseCookiesFeature(IFeatureCollection features)
: this(features, builderPool: null)
{ {
_features = new FeatureReferences<IHttpResponseFeature>(features);
} }
private IHttpResponseFeature HttpResponseFeature => /// <summary>
_features.Fetch(ref _features.Cache, f => null); /// Initializes a new <see cref="ResponseCookiesFeature"/> instance.
/// </summary>
/// <param name="features">
/// <see cref="IFeatureCollection"/> containing all defined features, including this
/// <see cref="IResponseCookiesFeature"/> and the <see cref="IHttpResponseFeature"/>.
/// </param>
/// <param name="builderPool">The <see cref="ObjectPool{T}"/>, if available.</param>
public ResponseCookiesFeature(IFeatureCollection features, ObjectPool<StringBuilder> builderPool)
{
if (features == null)
{
throw new ArgumentNullException(nameof(features));
}
_features = new FeatureReferences<IHttpResponseFeature>(features);
_builderPool = builderPool;
}
private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, f => null);
/// <inheritdoc />
public IResponseCookies Cookies public IResponseCookies Cookies
{ {
get get
@ -25,8 +60,9 @@ namespace Microsoft.AspNetCore.Http.Features.Internal
if (_cookiesCollection == null) if (_cookiesCollection == null)
{ {
var headers = HttpResponseFeature.Headers; var headers = HttpResponseFeature.Headers;
_cookiesCollection = new ResponseCookies(headers); _cookiesCollection = new ResponseCookies(headers, _builderPool);
} }
return _cookiesCollection; return _cookiesCollection;
} }
} }

View File

@ -1,30 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Internal;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Http.Internal namespace Microsoft.AspNetCore.Http.Internal
{ {
public class HttpContextFactory : IHttpContextFactory public class HttpContextFactory : IHttpContextFactory
{ {
private IHttpContextAccessor _httpContextAccessor; private readonly ObjectPool<StringBuilder> _builderPool;
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextFactory() : this(httpContextAccessor: null) public HttpContextFactory(ObjectPoolProvider poolProvider)
: this(poolProvider, httpContextAccessor: null)
{ {
} }
public HttpContextFactory(IHttpContextAccessor httpContextAccessor) public HttpContextFactory(ObjectPoolProvider poolProvider, IHttpContextAccessor httpContextAccessor)
{ {
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
_builderPool = poolProvider.CreateStringBuilderPool();
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
} }
public HttpContext Create(IFeatureCollection featureCollection) public HttpContext Create(IFeatureCollection featureCollection)
{ {
if (featureCollection == null)
{
throw new ArgumentNullException(nameof(featureCollection));
}
var responseCookiesFeature = new ResponseCookiesFeature(featureCollection, _builderPool);
featureCollection.Set<IResponseCookiesFeature>(responseCookiesFeature);
var httpContext = new DefaultHttpContext(featureCollection); var httpContext = new DefaultHttpContext(featureCollection);
if (_httpContextAccessor != null) if (_httpContextAccessor != null)
{ {
_httpContextAccessor.HttpContext = httpContext; _httpContextAccessor.HttpContext = httpContext;
} }
return httpContext; return httpContext;
} }

View File

@ -2,23 +2,27 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Text.Encodings.Web;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Http.Internal namespace Microsoft.AspNetCore.Http.Internal
{ {
/// <summary> /// <summary>
/// A wrapper for the response Set-Cookie header /// A wrapper for the response Set-Cookie header.
/// </summary> /// </summary>
public class ResponseCookies : IResponseCookies public class ResponseCookies : IResponseCookies
{ {
private readonly ObjectPool<StringBuilder> _builderPool;
/// <summary> /// <summary>
/// Create a new wrapper /// Create a new wrapper.
/// </summary> /// </summary>
/// <param name="headers"></param> /// <param name="headers">The <see cref="IHeaderDictionary"/> for the response.</param>
public ResponseCookies(IHeaderDictionary headers) /// <param name="builderPool">The <see cref="ObjectPool{T}"/>, if available.</param>
public ResponseCookies(IHeaderDictionary headers, ObjectPool<StringBuilder> builderPool)
{ {
if (headers == null) if (headers == null)
{ {
@ -26,33 +30,44 @@ namespace Microsoft.AspNetCore.Http.Internal
} }
Headers = headers; Headers = headers;
_builderPool = builderPool;
} }
private IHeaderDictionary Headers { get; set; } private IHeaderDictionary Headers { get; set; }
/// <summary> /// <inheritdoc />
/// Add a new cookie and value
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void Append(string key, string value) public void Append(string key, string value)
{ {
var setCookieHeaderValue = new SetCookieHeaderValue( var setCookieHeaderValue = new SetCookieHeaderValue(
Uri.EscapeDataString(key), Uri.EscapeDataString(key),
Uri.EscapeDataString(value)) Uri.EscapeDataString(value))
{ {
Path = "/" Path = "/"
}; };
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString()); string cookieValue;
if (_builderPool == null)
{
cookieValue = setCookieHeaderValue.ToString();
}
else
{
var stringBuilder = _builderPool.Get();
try
{
setCookieHeaderValue.AppendToStringBuilder(stringBuilder);
cookieValue = stringBuilder.ToString();
}
finally
{
_builderPool.Return(stringBuilder);
}
}
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
} }
/// <summary> /// <inheritdoc />
/// Add a new cookie
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
public void Append(string key, string value, CookieOptions options) public void Append(string key, string value, CookieOptions options)
{ {
if (options == null) if (options == null)
@ -61,8 +76,8 @@ namespace Microsoft.AspNetCore.Http.Internal
} }
var setCookieHeaderValue = new SetCookieHeaderValue( var setCookieHeaderValue = new SetCookieHeaderValue(
Uri.EscapeDataString(key), Uri.EscapeDataString(key),
Uri.EscapeDataString(value)) Uri.EscapeDataString(value))
{ {
Domain = options.Domain, Domain = options.Domain,
Path = options.Path, Path = options.Path,
@ -71,30 +86,42 @@ namespace Microsoft.AspNetCore.Http.Internal
HttpOnly = options.HttpOnly, HttpOnly = options.HttpOnly,
}; };
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString()); string cookieValue;
if (_builderPool == null)
{
cookieValue = setCookieHeaderValue.ToString();
}
else
{
var stringBuilder = _builderPool.Get();
try
{
setCookieHeaderValue.AppendToStringBuilder(stringBuilder);
cookieValue = stringBuilder.ToString();
}
finally
{
_builderPool.Return(stringBuilder);
}
}
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
} }
/// <summary> /// <inheritdoc />
/// Sets an expired cookie
/// </summary>
/// <param name="key"></param>
public void Delete(string key) public void Delete(string key)
{ {
Delete(key, new CookieOptions() { Path = "/" }); Delete(key, new CookieOptions() { Path = "/" });
} }
/// <summary> /// <inheritdoc />
/// Sets an expired cookie
/// </summary>
/// <param name="key"></param>
/// <param name="options"></param>
public void Delete(string key, CookieOptions options) public void Delete(string key, CookieOptions options)
{ {
if (options == null) if (options == null)
{ {
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "="; var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "=";
bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path); bool pathHasValue = !string.IsNullOrEmpty(options.Path);
@ -130,7 +157,7 @@ namespace Microsoft.AspNetCore.Http.Internal
newValues.Add(values[i]); newValues.Add(values[i]);
} }
} }
Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray());
} }

View File

@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
"Microsoft.AspNetCore.WebUtilities": "1.0.0-*", "Microsoft.AspNetCore.WebUtilities": "1.0.0-*",
"Microsoft.Extensions.ObjectPool": "1.0.0-*",
"Microsoft.Net.Http.Headers": "1.0.0-*" "Microsoft.Net.Http.Headers": "1.0.0-*"
}, },
"frameworks": { "frameworks": {

View File

@ -90,42 +90,54 @@ namespace Microsoft.Net.Http.Headers
public override string ToString() public override string ToString()
{ {
StringBuilder header = new StringBuilder(); StringBuilder header = new StringBuilder();
AppendToStringBuilder(header);
header.Append(_name); return header.ToString();
header.Append("="); }
header.Append(_value);
/// <summary>
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
/// <paramref name="builder"/>.
/// </summary>
/// <param name="builder">
/// The <see cref="StringBuilder"/> to receive the string representation of this
/// <see cref="SetCookieHeaderValue"/>.
/// </param>
public void AppendToStringBuilder(StringBuilder builder)
{
builder.Append(_name);
builder.Append("=");
builder.Append(_value);
if (Expires.HasValue) if (Expires.HasValue)
{ {
AppendSegment(header, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
} }
if (MaxAge.HasValue) if (MaxAge.HasValue)
{ {
AppendSegment(header, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds)); AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
} }
if (Domain != null) if (Domain != null)
{ {
AppendSegment(header, DomainToken, Domain); AppendSegment(builder, DomainToken, Domain);
} }
if (Path != null) if (Path != null)
{ {
AppendSegment(header, PathToken, Path); AppendSegment(builder, PathToken, Path);
} }
if (Secure) if (Secure)
{ {
AppendSegment(header, SecureToken, null); AppendSegment(builder, SecureToken, null);
} }
if (HttpOnly) if (HttpOnly)
{ {
AppendSegment(header, HttpOnlyToken, null); AppendSegment(builder, HttpOnlyToken, null);
} }
return header.ToString();
} }
private static void AppendSegment(StringBuilder builder, string name, string value) private static void AppendSegment(StringBuilder builder, string name, string value)

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.ObjectPool;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Http.Internal namespace Microsoft.AspNetCore.Http.Internal
@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Http.Internal
{ {
// Arrange // Arrange
var accessor = new HttpContextAccessor(); var accessor = new HttpContextAccessor();
var contextFactory = new HttpContextFactory(accessor); var contextFactory = new HttpContextFactory(new DefaultObjectPoolProvider(), accessor);
// Act // Act
var context = contextFactory.Create(new FeatureCollection()); var context = contextFactory.Create(new FeatureCollection());
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Http.Internal
public void AllowsCreatingContextWithoutSettingAccessor() public void AllowsCreatingContextWithoutSettingAccessor()
{ {
// Arrange // Arrange
var contextFactory = new HttpContextFactory(); var contextFactory = new HttpContextFactory(new DefaultObjectPoolProvider());
// Act & Assert // Act & Assert
var context = contextFactory.Create(new FeatureCollection()); var context = contextFactory.Create(new FeatureCollection());

View File

@ -1,19 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit; using System.Text;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Http.Tests namespace Microsoft.AspNetCore.Http.Tests
{ {
public class ResponseCookiesTest public class ResponseCookiesTest
{ {
[Fact] private static readonly ObjectPool<StringBuilder> _builderPool =
public void DeleteCookieShouldSetDefaultPath() new DefaultObjectPoolProvider().Create<StringBuilder>(new StringBuilderPooledObjectPolicy());
public static TheoryData BuilderPoolData
{
get
{
return new TheoryData<ObjectPool<StringBuilder>>
{
null,
_builderPool,
};
}
}
[Theory]
[MemberData(nameof(BuilderPoolData))]
public void DeleteCookieShouldSetDefaultPath(ObjectPool<StringBuilder> builderPool)
{ {
var headers = new HeaderDictionary(); var headers = new HeaderDictionary();
var cookies = new ResponseCookies(headers); var cookies = new ResponseCookies(headers, builderPool);
var testcookie = "TestCookie"; var testcookie = "TestCookie";
cookies.Delete(testcookie); cookies.Delete(testcookie);
@ -25,11 +43,12 @@ namespace Microsoft.AspNetCore.Http.Tests
Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]);
} }
[Fact] [Theory]
public void NoParamsDeleteRemovesCookieCreatedByAdd() [MemberData(nameof(BuilderPoolData))]
public void NoParamsDeleteRemovesCookieCreatedByAdd(ObjectPool<StringBuilder> builderPool)
{ {
var headers = new HeaderDictionary(); var headers = new HeaderDictionary();
var cookies = new ResponseCookies(headers); var cookies = new ResponseCookies(headers, builderPool);
var testcookie = "TestCookie"; var testcookie = "TestCookie";
cookies.Append(testcookie, testcookie); cookies.Append(testcookie, testcookie);
@ -42,14 +61,33 @@ namespace Microsoft.AspNetCore.Http.Tests
Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]);
} }
public static TheoryData EscapesKeyValuesBeforeSettingCookieData
{
get
{
// key, value, object pool, expected
return new TheoryData<string, string, ObjectPool<StringBuilder>, string>
{
{ "key", "value", null, "key=value" },
{ "key,", "!value", null, "key%2C=%21value" },
{ "ke#y,", "val^ue", null, "ke%23y%2C=val%5Eue" },
{ "key", "value", _builderPool, "key=value" },
{ "key,", "!value", _builderPool, "key%2C=%21value" },
{ "ke#y,", "val^ue", _builderPool, "ke%23y%2C=val%5Eue" },
};
}
}
[Theory] [Theory]
[InlineData("key", "value", "key=value")] [MemberData(nameof(EscapesKeyValuesBeforeSettingCookieData))]
[InlineData("key,", "!value", "key%2C=%21value")] public void EscapesKeyValuesBeforeSettingCookie(
[InlineData("ke#y,", "val^ue", "ke%23y%2C=val%5Eue")] string key,
public void EscapesKeyValuesBeforeSettingCookie(string key, string value, string expected) string value,
ObjectPool<StringBuilder> builderPool,
string expected)
{ {
var headers = new HeaderDictionary(); var headers = new HeaderDictionary();
var cookies = new ResponseCookies(headers); var cookies = new ResponseCookies(headers, builderPool);
cookies.Append(key, value); cookies.Append(key, value);

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Xunit; using Xunit;
namespace Microsoft.Net.Http.Headers namespace Microsoft.Net.Http.Headers
@ -264,6 +265,17 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(expectedValue, input.ToString()); Assert.Equal(expectedValue, input.ToString());
} }
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
{
var builder = new StringBuilder();
input.AppendToStringBuilder(builder);
Assert.Equal(expectedValue, builder.ToString());
}
[Theory] [Theory]
[MemberData(nameof(SetCookieHeaderDataSet))] [MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)