File: System\Net\Http\FormUrlEncodedContent.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.IO;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http
{
    public class FormUrlEncodedContent : ByteArrayContent
    {
        public FormUrlEncodedContent(
            IEnumerable<KeyValuePair<
                #nullable disable
                string, string
                #nullable restore
            >> nameValueCollection)
            : base(GetContentByteArray(nameValueCollection))
        {
            Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
        }
 
        private static byte[] GetContentByteArray(IEnumerable<KeyValuePair<string?, string?>> nameValueCollection)
        {
            ArgumentNullException.ThrowIfNull(nameValueCollection);
 
            // Encode and concatenate data
            var builder = new ValueStringBuilder(stackalloc char[256]);
 
            foreach (KeyValuePair<string?, string?> pair in nameValueCollection)
            {
                if (builder.Length > 0)
                {
                    builder.Append('&');
                }
 
                Encode(ref builder, pair.Key);
                builder.Append('=');
                Encode(ref builder, pair.Value);
            }
 
            // EscapeDataString will always return an ASCII string and DefaultHttpEncoding is Latin1,
            // so we know the output byte size will be the same as the builder length.
            byte[] bytes = new byte[builder.Length];
            HttpRuleParser.DefaultHttpEncoding.GetBytes(builder.AsSpan(), bytes);
            builder.Dispose();
            return bytes;
        }
 
        private static void Encode(ref ValueStringBuilder builder, string? data)
        {
            if (!string.IsNullOrEmpty(data))
            {
                int charsWritten;
                while (!Uri.TryEscapeDataString(data, builder.RawChars.Slice(builder.Length), out charsWritten))
                {
                    builder.EnsureCapacity(builder.Capacity + 1);
                }
 
                // Escape spaces as '+'.
                if (data.Contains(' '))
                {
                    ReadOnlySpan<char> escapedChars = builder.RawChars.Slice(builder.Length, charsWritten);
 
                    while (true)
                    {
                        int indexOfEscapedSpace = escapedChars.IndexOf("%20", StringComparison.Ordinal);
                        if (indexOfEscapedSpace < 0)
                        {
                            builder.Append(escapedChars);
                            break;
                        }
 
                        builder.Append(escapedChars.Slice(0, indexOfEscapedSpace));
                        builder.Append('+');
                        escapedChars = escapedChars.Slice(indexOfEscapedSpace + 3); // Skip "%20"
                    }
                }
                else
                {
                    builder.Length += charsWritten;
                }
            }
        }
 
        protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
            // Only skip the original protected virtual SerializeToStreamAsync if this
            // isn't a derived type that may have overridden the behavior.
            GetType() == typeof(FormUrlEncodedContent) ? SerializeToStreamAsyncCore(stream, cancellationToken) :
            base.SerializeToStreamAsync(stream, context, cancellationToken);
 
        internal override Stream? TryCreateContentReadStream() =>
            GetType() == typeof(FormUrlEncodedContent) ? CreateMemoryStreamForByteArray() : // type check ensures we use possible derived type's CreateContentReadStreamAsync override
            null;
    }
}