3
3
// See the LICENSE file in the project root for more information
4
4
5
5
using System ;
6
+ using System . Buffers ;
6
7
using System . Collections . Generic ;
7
8
using System . Collections . Specialized ;
8
9
using System . Linq ;
@@ -12,28 +13,58 @@ namespace Elasticsearch.Net.Extensions
12
13
{
13
14
internal static class NameValueCollectionExtensions
14
15
{
16
+ private const int MaxCharsOnStack = 256 ; // 512 bytes
17
+
15
18
internal static string ToQueryString ( this NameValueCollection nv )
16
19
{
17
20
if ( nv == null || nv . AllKeys . Length == 0 ) return string . Empty ;
18
21
19
- // initialize with capacity for number of key/values with length 5 each
20
- var builder = new StringBuilder ( "?" , nv . AllKeys . Length * 2 * 5 ) ;
21
- for ( var i = 0 ; i < nv . AllKeys . Length ; i ++ )
22
+ var maxLength = 1 + nv . AllKeys . Length - 1 ; // account for '?', and any required '&' chars
23
+ foreach ( var key in nv . AllKeys )
22
24
{
23
- if ( i != 0 )
24
- builder . Append ( "&" ) ;
25
+ var bytes = Encoding . UTF8 . GetByteCount ( key ) + Encoding . UTF8 . GetByteCount ( nv [ key ] ?? string . Empty ) ;
26
+ var maxEncodedSize = bytes * 3 ; // worst case, assume all bytes are URL escaped to 3 chars
27
+ maxLength += 1 + maxEncodedSize ; // '=' + encoded chars
28
+ }
25
29
26
- var key = nv . AllKeys [ i ] ;
27
- builder . Append ( Uri . EscapeDataString ( key ) ) ;
28
- var value = nv [ key ] ;
29
- if ( ! value . IsNullOrEmpty ( ) )
30
+ // prefer stack allocated array for short lengths
31
+ // note: renting for larger lengths is slightly more efficient since no zeroing occurs
32
+ char [ ] rentedFromPool = null ;
33
+ var buffer = maxLength > MaxCharsOnStack
34
+ ? rentedFromPool = ArrayPool < char > . Shared . Rent ( maxLength )
35
+ : stackalloc char [ maxLength ] ;
36
+
37
+ try
38
+ {
39
+ var position = 0 ;
40
+ buffer [ position ++ ] = '?' ;
41
+
42
+ foreach ( var key in nv . AllKeys )
30
43
{
31
- builder . Append ( "=" ) ;
32
- builder . Append ( Uri . EscapeDataString ( nv [ key ] ) ) ;
44
+ if ( position != 1 )
45
+ buffer [ position ++ ] = '&' ;
46
+
47
+ var escapedKey = Uri . EscapeDataString ( key ) ;
48
+ escapedKey . AsSpan ( ) . CopyTo ( buffer . Slice ( position ) ) ;
49
+ position += escapedKey . Length ;
50
+
51
+ var value = nv [ key ] ;
52
+
53
+ if ( value . IsNullOrEmpty ( ) ) continue ;
54
+
55
+ buffer [ position ++ ] = '=' ;
56
+ var escapedValue = Uri . EscapeDataString ( value ) ;
57
+ escapedValue . AsSpan ( ) . CopyTo ( buffer . Slice ( position ) ) ;
58
+ position += escapedValue . Length ;
33
59
}
34
- }
35
60
36
- return builder . ToString ( ) ;
61
+ return buffer . Slice ( 0 , position ) . ToString ( ) ;
62
+ }
63
+ finally
64
+ {
65
+ if ( rentedFromPool is object )
66
+ ArrayPool < char > . Shared . Return ( rentedFromPool , clearArray : false ) ;
67
+ }
37
68
}
38
69
39
70
internal static void UpdateFromDictionary ( this NameValueCollection queryString , Dictionary < string , object > queryStringUpdates ,
0 commit comments