Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

852 lines
31 KiB

/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.ObjectModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Primitives;
namespace OpenIddict.Extensions;
/// <summary>
/// Exposes common helpers used by the OpenIddict assemblies.
/// </summary>
internal static class OpenIddictHelpers
{
/// <summary>
/// Determines whether the specified array contains at least one value present in the specified set.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="array">The array.</param>
/// <param name="set">The set.</param>
/// <returns>
/// <see langword="true"/> if the specified array contains at least one
/// value present in the specified set, <see langword="false"/> otherwise.
/// </returns>
public static bool IncludesAnyFromSet<T>(IReadOnlyList<T> array, ISet<T> set)
{
ArgumentNullException.ThrowIfNull(set);
for (var index = 0; index < array.Count; index++)
{
var value = array[index];
if (set.Contains(value))
{
return true;
}
}
return false;
}
/// <summary>
/// Determines whether the specified <paramref name="exception"/> is considered fatal.
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns>
/// <see langword="true"/> if the exception is considered fatal, <see langword="false"/> otherwise.
/// </returns>
public static bool IsFatal(Exception exception)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
return exception switch
{
ThreadAbortException => true,
OutOfMemoryException and not InsufficientMemoryException => true,
AggregateException { InnerExceptions: var exceptions } => IsAnyFatal(exceptions),
Exception { InnerException: Exception inner } => IsFatal(inner),
_ => false
};
static bool IsAnyFatal(ReadOnlyCollection<Exception> exceptions)
{
for (var index = 0; index < exceptions.Count; index++)
{
if (IsFatal(exceptions[index]))
{
return true;
}
}
return false;
}
}
/// <summary>
/// Computes an absolute URI from the specified <paramref name="left"/> and <paramref name="right"/> URIs.
/// Note: if the <paramref name="right"/> URI is already absolute, it is directly returned.
/// </summary>
/// <param name="left">The left part.</param>
/// <param name="right">The right part.</param>
/// <returns>An absolute URI from the specified <paramref name="left"/> and <paramref name="right"/>.</returns>
/// <exception cref="InvalidOperationException"><paramref name="left"/> is not an absolute URI.</exception>
[return: NotNullIfNotNull(nameof(right))]
public static Uri? CreateAbsoluteUri(Uri? left, string? right)
=> CreateAbsoluteUri(left, !string.IsNullOrEmpty(right) ? new Uri(right, UriKind.RelativeOrAbsolute) : null);
/// <summary>
/// Computes an absolute URI from the specified <paramref name="left"/> and <paramref name="right"/> URIs.
/// Note: if the <paramref name="right"/> URI is already absolute, it is directly returned.
/// </summary>
/// <param name="left">The left part.</param>
/// <param name="right">The right part.</param>
/// <returns>An absolute URI from the specified <paramref name="left"/> and <paramref name="right"/>.</returns>
/// <exception cref="InvalidOperationException"><paramref name="left"/> is not an absolute URI.</exception>
[return: NotNullIfNotNull(nameof(right))]
public static Uri? CreateAbsoluteUri(Uri? left, Uri? right)
{
if (right is null)
{
return null;
}
if (right.IsAbsoluteUri)
{
return right;
}
if (left is not { IsAbsoluteUri: true })
{
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left));
}
// Ensure the left part ends with a trailing slash, as it is necessary
// for Uri's constructor to include the last path segment in the base URI.
left = left.AbsolutePath switch
{
null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri,
[.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri,
['/'] or _ => left
};
return new Uri(left, right);
}
/// <summary>
/// Determines whether the <paramref name="left"/> URI is a base of the <paramref name="right"/> URI.
/// </summary>
/// <param name="left">The left part.</param>
/// <param name="right">The right part.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is base of
/// <paramref name="right"/>, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="left"/> or
/// <paramref name="right"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="left"/> is not an absolute URI.</exception>
public static bool IsBaseOf(Uri left, Uri right)
{
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
if (left is not { IsAbsoluteUri: true })
{
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left));
}
// Ensure the left part ends with a trailing slash, as it is necessary
// for Uri's constructor to include the last path segment in the base URI.
left = left.AbsolutePath switch
{
null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri,
[.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri,
['/'] or _ => left
};
return left.IsBaseOf(right);
}
/// <summary>
/// Determines whether the specified <paramref name="uri"/> represents an implicit file URI.
/// </summary>
/// <param name="uri">The URI.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="uri"/> represents
/// an implicit file URI, <see langword="false"/> otherwise.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
public static bool IsImplicitFileUri(Uri uri)
{
ArgumentNullException.ThrowIfNull(uri);
return uri.IsAbsoluteUri && uri.IsFile &&
!uri.OriginalString.StartsWith(uri.Scheme, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Adds a query string parameter to the specified <see cref="Uri"/>.
/// </summary>
/// <param name="uri">The URI to which the query string parameter will be appended.</param>
/// <param name="name">The name of the query string parameter to append.</param>
/// <param name="value">The value of the query string parameter to append.</param>
/// <returns>The final <see cref="Uri"/> instance, with the specified parameter appended.</returns>
public static Uri AddQueryStringParameter(Uri uri, string name, string? value)
{
ArgumentNullException.ThrowIfNull(uri);
var builder = new StringBuilder(uri.Query);
if (builder.Length is > 0)
{
builder.Append('&');
}
builder.Append(Uri.EscapeDataString(name));
if (!string.IsNullOrEmpty(value))
{
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
return new UriBuilder(uri) { Query = builder.ToString() }.Uri;
}
/// <summary>
/// Adds query string parameters to the specified <see cref="Uri"/>.
/// </summary>
/// <param name="uri">The URI to which the query string parameters will be appended.</param>
/// <param name="parameters">The query string parameters to append.</param>
/// <returns>The final <see cref="Uri"/> instance, with the specified parameters appended.</returns>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="parameters"/> is <see langword="null"/>.</exception>
public static Uri AddQueryStringParameters(Uri uri, IReadOnlyDictionary<string, StringValues> parameters)
{
ArgumentNullException.ThrowIfNull(uri);
ArgumentNullException.ThrowIfNull(parameters);
if (parameters.Count is 0)
{
return uri;
}
var builder = new StringBuilder(uri.Query);
foreach (var parameter in parameters)
{
// If the parameter doesn't include any string value,
// only append the parameter key to the query string.
if (parameter.Value.Count is 0)
{
if (builder.Length is > 0)
{
builder.Append('&');
}
builder.Append(Uri.EscapeDataString(parameter.Key));
}
// Otherwise, iterate the string values and create
// a new "name=value" pair for each iterated value.
else
{
foreach (var value in parameter.Value)
{
if (builder.Length is > 0)
{
builder.Append('&');
}
builder.Append(Uri.EscapeDataString(parameter.Key));
if (!string.IsNullOrEmpty(value))
{
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
}
return new UriBuilder(uri) { Query = builder.ToString() }.Uri;
}
/// <summary>
/// Extracts the parameters from the specified query string.
/// </summary>
/// <param name="query">The query string, which may start with a '?'.</param>
/// <returns>The parameters extracted from the specified query string.</returns>
/// <exception cref="ArgumentNullException"><paramref name="query"/> is <see langword="null"/>.</exception>
public static IReadOnlyDictionary<string, StringValues> ParseQuery(string query)
{
ArgumentNullException.ThrowIfNull(query);
return query.TrimStart(Separators.QuestionMark[0])
.Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries)
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
}
/// <summary>
/// Extracts the parameters from the specified fragment.
/// </summary>
/// <param name="fragment">The fragment string, which may start with a '#'.</param>
/// <returns>The parameters extracted from the specified fragment.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fragment"/> is <see langword="null"/>.</exception>
public static IReadOnlyDictionary<string, StringValues> ParseFragment(string fragment)
{
ArgumentNullException.ThrowIfNull(fragment);
return fragment.TrimStart(Separators.Hash[0])
.Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries)
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
}
/// <summary>
/// Extracts the parameters from the specified stream.
/// </summary>
/// <param name="stream">The stream containing the formurl-encoded data.</param>
/// <param name="encoding">The encoding used to decode the data.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The parameters extracted from the specified stream.</returns>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is <see langword="null"/>.</exception>
public static async ValueTask<IReadOnlyDictionary<string, StringValues>> ParseFormAsync(
Stream stream, Encoding encoding, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentNullException.ThrowIfNull(encoding);
var reader = new FormReader(stream, encoding);
return await reader.ReadFormAsync(cancellationToken);
}
/// <summary>
/// Removes the characters that are not part of <paramref name="charset"/>
/// from the specified <paramref name="value"/> string.
/// </summary>
/// <remarks>
/// Note: if no character is present in <paramref name="charset"/>, all characters are considered valid.
/// </remarks>
/// <param name="value">The original string.</param>
/// <param name="charset">The list of allowed characters.</param>
/// <returns>The original string with the disallowed characters removed.</returns>
/// <exception cref="ArgumentNullException"><paramref name="charset"/> is <see langword="null"/>.</exception>
public static string? RemoveDisallowedCharacters(string? value, IReadOnlyCollection<string> charset)
{
ArgumentNullException.ThrowIfNull(charset);
if (charset.Count is 0 || string.IsNullOrEmpty(value))
{
return value;
}
var builder = new StringBuilder();
var enumerator = StringInfo.GetTextElementEnumerator(value);
while (enumerator.MoveNext())
{
var element = enumerator.GetTextElement();
if (charset.Contains(element))
{
builder.Append(element);
}
}
return builder.ToString();
}
/// <summary>
/// Determines whether the specified <paramref name="element"/> represents a null, undefined or empty JSON node.
/// </summary>
/// <param name="element">The <see cref="JsonElement"/>.</param>
/// <returns>
/// <see langword="true"/> if the JSON node is null, undefined or empty <see langword="false"/> otherwise.
/// </returns>
public static bool IsNullOrEmpty(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Undefined or JsonValueKind.Null => true,
JsonValueKind.String => string.IsNullOrEmpty(element.GetString()),
JsonValueKind.Array => element.GetArrayLength() is 0,
JsonValueKind.Object => element.GetPropertyCount() is 0,
_ => false,
};
/// <summary>
/// Determines whether the specified <paramref name="node"/> represents a null or empty JSON node.
/// </summary>
/// <param name="node">The <see cref="JsonNode"/>.</param>
/// <returns>
/// <see langword="true"/> if the JSON node is null or empty, <see langword="false"/> otherwise.
/// </returns>
public static bool IsNullOrEmpty([NotNullWhen(false)] JsonNode? node) => node switch
{
null => true,
JsonArray value => value.Count is 0,
JsonObject value => value.Count is 0,
JsonValue value when value.TryGetValue(out string? result) => string.IsNullOrEmpty(result),
JsonValue value when value.TryGetValue(out JsonElement element) => IsNullOrEmpty(element),
// If the JSON node cannot be mapped to a primitive type, convert it to
// a JsonElement instance and infer the corresponding claim value type.
JsonNode value => IsNullOrEmpty(value.Deserialize(OpenIddictSerializer.Default.JsonElement))
};
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is a certificate authority.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is a certificate authority, <see langword="false"/> otherwise.
/// </returns>
public static bool IsCertificateAuthority(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.Extensions.OfType<X509BasicConstraintsExtension>()
.Any(static extension => extension.CertificateAuthority);
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> has the specified extended key usage.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <param name="usage">The extended key usage.</param>
/// <returns>
/// <see langword="true"/> if the certificate has the specified extended key usage, <see langword="false"/> otherwise.
/// </returns>
public static bool HasExtendedKeyUsage(X509Certificate2 certificate, string usage)
{
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
HasOid(extension.EnhancedKeyUsages, usage))
{
return true;
}
}
return false;
static bool HasOid(OidCollection collection, string value)
{
for (var index = 0; index < collection.Count; index++)
{
if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> has the specified key usage.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <param name="usage">The <see cref="X509KeyUsageFlags"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate has the specified key usage, <see langword="false"/> otherwise.
/// </returns>
public static bool HasKeyUsage(X509Certificate2 certificate, X509KeyUsageFlags usage)
{
ArgumentNullException.ThrowIfNull(certificate);
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
extension.KeyUsages.HasFlag(usage))
{
return true;
}
}
return false;
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is self-issued.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is self-issued, <see langword="false"/> otherwise.
/// </returns>
public static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is suitable for client authentication.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is suitable for client authentication, <see langword="false"/> otherwise.
/// </returns>
public static bool IsClientAuthenticationCertificate(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.Version is >= 3 &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
}
/// <summary>
/// Determines whether the items contained in <paramref name="element"/>
/// are of the specified <paramref name="kind"/>.
/// </summary>
/// <param name="element">The <see cref="JsonElement"/>.</param>
/// <param name="kind">The expected <see cref="JsonValueKind"/>.</param>
/// <returns>
/// <see langword="true"/> if the array doesn't contain any value or if all the items
/// are of the specified <paramref name="kind"/>, <see langword="false"/> otherwise.
/// </returns>
public static bool ValidateArrayElements(JsonElement element, JsonValueKind kind)
{
if (element.ValueKind is not JsonValueKind.Array)
{
throw new ArgumentOutOfRangeException(nameof(element));
}
foreach (var item in element.EnumerateArray())
{
if (item.ValueKind != kind)
{
return false;
}
}
return true;
}
/// <summary>
/// Determines whether the items contained in <paramref name="element"/>
/// are of the specified <paramref name="kind"/>.
/// </summary>
/// <param name="element">The <see cref="JsonElement"/>.</param>
/// <param name="kind">The expected <see cref="JsonValueKind"/>.</param>
/// <returns>
/// <see langword="true"/> if the object doesn't contain any value or if all the items
/// are of the specified <paramref name="kind"/>, <see langword="false"/> otherwise.
/// </returns>
public static bool ValidateObjectElements(JsonElement element, JsonValueKind kind)
{
if (element.ValueKind is not JsonValueKind.Object)
{
throw new ArgumentOutOfRangeException(nameof(element));
}
foreach (var property in element.EnumerateObject())
{
if (property.Value.ValueKind != kind)
{
return false;
}
}
return true;
}
/// <summary>
/// Note: this implementation was taken from ASP.NET Core.
/// </summary>
private class FormReader
{
public const int DefaultValueCountLimit = 1024;
public const int DefaultKeyLengthLimit = 1024 * 2;
public const int DefaultValueLengthLimit = 1024 * 1024 * 4;
private readonly TextReader _reader;
private readonly char[] _buffer;
private readonly StringBuilder _builder = new();
private int _bufferOffset;
private int _bufferCount;
private string? _currentKey;
private string? _currentValue;
private bool _endOfStream;
public FormReader(Stream stream, Encoding encoding)
{
_buffer = new char[8192];
_reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true);
}
public int ValueCountLimit { get; set; } = DefaultValueCountLimit;
public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit;
public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit;
public KeyValuePair<string, string>? ReadNextPair()
{
ReadNextPairImpl();
if (ReadSucceeded())
{
return KeyValuePair.Create(_currentKey, _currentValue);
}
return null;
}
private void ReadNextPairImpl()
{
StartReadNextPair();
while (!_endOfStream)
{
// Empty
if (_bufferCount == 0)
{
Buffer();
}
if (TryReadNextPair())
{
break;
}
}
}
public async Task<KeyValuePair<string, string>?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken())
{
await ReadNextPairAsyncImpl(cancellationToken);
if (ReadSucceeded())
{
return KeyValuePair.Create(_currentKey, _currentValue);
}
return null;
}
private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken())
{
StartReadNextPair();
while (!_endOfStream)
{
if (_bufferCount == 0)
{
await BufferAsync(cancellationToken);
}
if (TryReadNextPair())
{
break;
}
}
}
private void StartReadNextPair()
{
_currentKey = null;
_currentValue = null;
}
private bool TryReadNextPair()
{
if (_currentKey == null)
{
if (!TryReadWord('=', KeyLengthLimit, out _currentKey))
{
return false;
}
if (_bufferCount == 0)
{
return false;
}
}
if (_currentValue == null)
{
if (!TryReadWord('&', ValueLengthLimit, out _currentValue))
{
return false;
}
}
return true;
}
private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value)
{
do
{
if (ReadChar(separator, limit, out value))
{
return true;
}
} while (_bufferCount > 0);
return false;
}
private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word)
{
if (_bufferCount == 0)
{
word = BuildWord();
return true;
}
var c = _buffer[_bufferOffset++];
_bufferCount--;
if (c == separator)
{
word = BuildWord();
return true;
}
if (_builder.Length >= limit)
{
throw new InvalidDataException($"Form key or value length limit {limit} exceeded.");
}
_builder.Append(c);
word = null;
return false;
}
private string BuildWord()
{
_builder.Replace('+', ' ');
var result = _builder.ToString();
_builder.Clear();
return Uri.UnescapeDataString(result);
}
private void Buffer()
{
_bufferOffset = 0;
_bufferCount = _reader.Read(_buffer, 0, _buffer.Length);
_endOfStream = _bufferCount == 0;
}
private async Task BufferAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_bufferOffset = 0;
_bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length);
_endOfStream = _bufferCount == 0;
}
public Dictionary<string, StringValues> ReadForm()
{
var accumulator = new KeyValueAccumulator();
while (!_endOfStream)
{
ReadNextPairImpl();
Append(ref accumulator);
}
return accumulator.GetResults();
}
public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken())
{
var accumulator = new KeyValueAccumulator();
while (!_endOfStream)
{
await ReadNextPairAsyncImpl(cancellationToken);
Append(ref accumulator);
}
return accumulator.GetResults();
}
[MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))]
private bool ReadSucceeded()
{
return _currentKey != null && _currentValue != null;
}
private void Append(ref KeyValueAccumulator accumulator)
{
if (ReadSucceeded())
{
accumulator.Append(_currentKey, _currentValue);
if (accumulator.ValueCount > ValueCountLimit)
{
throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded.");
}
}
}
}
/// <summary>
/// Note: this implementation was taken from ASP.NET Core.
/// </summary>
private struct KeyValueAccumulator
{
private Dictionary<string, StringValues> _accumulator;
private Dictionary<string, List<string>> _expandingAccumulator;
public void Append(string key, string value)
{
if (_accumulator == null)
{
_accumulator = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
}
StringValues values;
if (_accumulator.TryGetValue(key, out values))
{
if (values.Count == 0)
{
_expandingAccumulator[key].Add(value);
}
else if (values.Count == 1)
{
_accumulator[key] = new string[] { values[0]!, value };
}
else
{
_accumulator[key] = default(StringValues);
if (_expandingAccumulator == null)
{
_expandingAccumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
}
var list = new List<string>(8);
var array = values.ToArray();
list.Add(array[0]!);
list.Add(array[1]!);
list.Add(value);
_expandingAccumulator[key] = list;
}
}
else
{
_accumulator[key] = new StringValues(value);
}
ValueCount++;
}
public bool HasValues => ValueCount > 0;
public int KeyCount => _accumulator?.Count ?? 0;
public int ValueCount { get; private set; }
public Dictionary<string, StringValues> GetResults()
{
if (_expandingAccumulator != null)
{
foreach (var entry in _expandingAccumulator)
{
_accumulator[entry.Key] = new StringValues([.. entry.Value]);
}
}
return _accumulator ?? new Dictionary<string, StringValues>(0, StringComparer.OrdinalIgnoreCase);
}
}
}