diff --git a/src/Microsoft.AspNetCore.Http.Features/IResponseCookiesFeature.cs b/src/Microsoft.AspNetCore.Http.Features/IResponseCookiesFeature.cs index fb5ea092..3ec15afe 100644 --- a/src/Microsoft.AspNetCore.Http.Features/IResponseCookiesFeature.cs +++ b/src/Microsoft.AspNetCore.Http.Features/IResponseCookiesFeature.cs @@ -3,8 +3,33 @@ namespace Microsoft.AspNetCore.Http.Features { + /// + /// Feature containing cookies which will be returned with the response. + /// public interface IResponseCookiesFeature { + /// + /// Initial capacity of instances used in the implementation of this feature, in s. + /// + /// + /// For example, the initial capacity of instances obtained + /// from an object pool. + /// + int InitialPooledInstanceCapacity { get; set; } + + /// + /// Maximum retained capacity of instances used in the implementation of this feature, in s. + /// Instances larger than this will be deleted rather than preserved in the pool. + /// + /// + /// For example, the maximum retained capacity of instances obtained + /// from an object pool. + /// + int MaximumRetainedPooledInstanceCapacity { get; set; } + + /// + /// Cookies which will be returned with the response. + /// IResponseCookies Cookies { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs b/src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs index 136ecd2a..2052efa9 100644 --- a/src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs +++ b/src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs @@ -2,22 +2,46 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Http.Features.Internal { + /// + /// Default implementation of . + /// public class ResponseCookiesFeature : IResponseCookiesFeature { private FeatureReferences _features; + private FeatureReferences _services; private IResponseCookies _cookiesCollection; + /// + /// Initializes a new instance. + /// + /// + /// containing all defined features, including this + /// and the . + /// public ResponseCookiesFeature(IFeatureCollection features) { _features = new FeatureReferences(features); + _services = new FeatureReferences(features); } - private IHttpResponseFeature HttpResponseFeature => - _features.Fetch(ref _features.Cache, f => null); + /// + /// Default is 100 s. + public int InitialPooledInstanceCapacity { get; set; } = new StringBuilderPooledObjectPolicy().InitialCapacity; + /// + /// Default is 4048 s. + public int MaximumRetainedPooledInstanceCapacity { get; set; } = + new StringBuilderPooledObjectPolicy().MaximumRetainedCapacity; + + private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, f => null); + + private IServiceProvidersFeature ServiceProvidersFeature => _services.Fetch(ref _services.Cache, f => null); + + /// public IResponseCookies Cookies { get @@ -25,8 +49,17 @@ public IResponseCookies Cookies if (_cookiesCollection == null) { var headers = HttpResponseFeature.Headers; - _cookiesCollection = new ResponseCookies(headers); + + var serviceProvider = ServiceProvidersFeature.RequestServices; + var provider = (ObjectPoolProvider)serviceProvider.GetService(typeof(ObjectPoolProvider)) ?? + new DefaultObjectPoolProvider(); + var pool = provider.CreateStringBuilderPool( + InitialPooledInstanceCapacity, + MaximumRetainedPooledInstanceCapacity); + + _cookiesCollection = new ResponseCookies(headers, pool); } + return _cookiesCollection; } } diff --git a/src/Microsoft.AspNetCore.Http/ResponseCookies.cs b/src/Microsoft.AspNetCore.Http/ResponseCookies.cs index 16832c7d..c374e24a 100644 --- a/src/Microsoft.AspNetCore.Http/ResponseCookies.cs +++ b/src/Microsoft.AspNetCore.Http/ResponseCookies.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text.Encodings.Web; using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -14,18 +15,26 @@ namespace Microsoft.AspNetCore.Http.Internal /// public class ResponseCookies : IResponseCookies { + private readonly ObjectPool _builderPool; + /// /// Create a new wrapper /// - /// - public ResponseCookies(IHeaderDictionary headers) + /// The for the response. + /// The used. + public ResponseCookies(IHeaderDictionary headers, ObjectPool builderPool) { if (headers == null) { throw new ArgumentNullException(nameof(headers)); } + if (builderPool == null) + { + throw new ArgumentNullException(nameof(builderPool)); + } Headers = headers; + _builderPool = builderPool; } private IHeaderDictionary Headers { get; set; } @@ -38,13 +47,25 @@ public ResponseCookies(IHeaderDictionary headers) public void Append(string key, string value) { var setCookieHeaderValue = new SetCookieHeaderValue( - Uri.EscapeDataString(key), - Uri.EscapeDataString(value)) + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) { Path = "/" }; - Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString()); + string cookieValue; + var stringBuilder = _builderPool.Get(); + try + { + setCookieHeaderValue.AppendToStringBuilder(stringBuilder); + cookieValue = stringBuilder.ToString(); + } + finally + { + _builderPool.Return(stringBuilder); + } + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); } /// @@ -61,8 +82,8 @@ public void Append(string key, string value, CookieOptions options) } var setCookieHeaderValue = new SetCookieHeaderValue( - Uri.EscapeDataString(key), - Uri.EscapeDataString(value)) + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) { Domain = options.Domain, Path = options.Path, @@ -71,7 +92,19 @@ public void Append(string key, string value, CookieOptions options) HttpOnly = options.HttpOnly, }; - Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString()); + string cookieValue; + var stringBuilder = _builderPool.Get(); + try + { + setCookieHeaderValue.AppendToStringBuilder(stringBuilder); + cookieValue = stringBuilder.ToString(); + } + finally + { + _builderPool.Return(stringBuilder); + } + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); } /// @@ -94,7 +127,7 @@ public void Delete(string key, CookieOptions options) { throw new ArgumentNullException(nameof(options)); } - + var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "="; bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); @@ -130,7 +163,7 @@ public void Delete(string key, CookieOptions options) newValues.Add(values[i]); } } - + Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); } diff --git a/src/Microsoft.AspNetCore.Http/project.json b/src/Microsoft.AspNetCore.Http/project.json index 8e1578e2..0052abaf 100644 --- a/src/Microsoft.AspNetCore.Http/project.json +++ b/src/Microsoft.AspNetCore.Http/project.json @@ -17,6 +17,7 @@ "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.WebUtilities": "1.0.0-*", + "Microsoft.Extensions.ObjectPool": "1.0.0-*", "Microsoft.Net.Http.Headers": "1.0.0-*" }, "frameworks": { diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs index 37372e01..7f790d66 100644 --- a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs @@ -90,42 +90,54 @@ public string Value public override string ToString() { StringBuilder header = new StringBuilder(); + AppendToStringBuilder(header); - header.Append(_name); - header.Append("="); - header.Append(_value); + return header.ToString(); + } + + /// + /// Append string representation of this to given + /// . + /// + /// + /// The to receive the string representation of this + /// . + /// + public void AppendToStringBuilder(StringBuilder builder) + { + builder.Append(_name); + builder.Append("="); + builder.Append(_value); if (Expires.HasValue) { - AppendSegment(header, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); + AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); } if (MaxAge.HasValue) { - AppendSegment(header, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds)); + AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds)); } if (Domain != null) { - AppendSegment(header, DomainToken, Domain); + AppendSegment(builder, DomainToken, Domain); } if (Path != null) { - AppendSegment(header, PathToken, Path); + AppendSegment(builder, PathToken, Path); } if (Secure) { - AppendSegment(header, SecureToken, null); + AppendSegment(builder, SecureToken, null); } if (HttpOnly) { - AppendSegment(header, HttpOnlyToken, null); + AppendSegment(builder, HttpOnlyToken, null); } - - return header.ToString(); } private static void AppendSegment(StringBuilder builder, string name, string value) diff --git a/test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs b/test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs index cc26d657..5d7a73fc 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs @@ -1,19 +1,24 @@ // 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 Xunit; -using Microsoft.Net.Http.Headers; +using System.Text; using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Net.Http.Headers; +using Xunit; namespace Microsoft.AspNetCore.Http.Tests { public class ResponseCookiesTest { + private static readonly ObjectPool _builderPool = + new DefaultObjectPoolProvider().Create(new StringBuilderPooledObjectPolicy()); + [Fact] public void DeleteCookieShouldSetDefaultPath() { var headers = new HeaderDictionary(); - var cookies = new ResponseCookies(headers); + var cookies = new ResponseCookies(headers, _builderPool); var testcookie = "TestCookie"; cookies.Delete(testcookie); @@ -29,7 +34,7 @@ public void DeleteCookieShouldSetDefaultPath() public void NoParamsDeleteRemovesCookieCreatedByAdd() { var headers = new HeaderDictionary(); - var cookies = new ResponseCookies(headers); + var cookies = new ResponseCookies(headers, _builderPool); var testcookie = "TestCookie"; cookies.Append(testcookie, testcookie); @@ -49,7 +54,7 @@ public void NoParamsDeleteRemovesCookieCreatedByAdd() public void EscapesKeyValuesBeforeSettingCookie(string key, string value, string expected) { var headers = new HeaderDictionary(); - var cookies = new ResponseCookies(headers); + var cookies = new ResponseCookies(headers, _builderPool); cookies.Append(key, value); diff --git a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs index 5d2c1385..a3dad09a 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Xunit; namespace Microsoft.Net.Http.Headers @@ -264,6 +265,17 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp 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] [MemberData(nameof(SetCookieHeaderDataSet))] public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)