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:
parent
8efc650e74
commit
80813f7c1e
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue