From 2337bf551f387ff7b8b06fe71e96d7c39ff98805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Fri, 2 Jul 2021 18:57:52 +0200 Subject: [PATCH 01/15] Use inline Vector128.Create for constants (#33969) --- .../ServerInfrastructure/StringUtilities.cs | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/Shared/ServerInfrastructure/StringUtilities.cs b/src/Shared/ServerInfrastructure/StringUtilities.cs index 87fb890d5da6..fa802f8d66a9 100644 --- a/src/Shared/ServerInfrastructure/StringUtilities.cs +++ b/src/Shared/ServerInfrastructure/StringUtilities.cs @@ -132,13 +132,13 @@ public static unsafe bool TryGetAsciiString(byte* input, char* output, int count Debug.Assert((long)end >= Vector256.Count); // PERF: so the JIT can reuse the zero from a register - Vector128 zero = Vector128.Zero; + var zero = Vector128.Zero; if (Sse2.IsSupported) { if (Avx2.IsSupported && input <= end - Vector256.Count) { - Vector256 avxZero = Vector256.Zero; + var avxZero = Vector256.Zero; do { @@ -233,8 +233,8 @@ out Unsafe.AsRef>(output), // BMI2 could be used, but this variant is faster on both Intel and AMD. if (Sse2.X64.IsSupported) { - Vector128 vecNarrow = Sse2.X64.ConvertScalarToVector128Int64(value).AsSByte(); - Vector128 vecWide = Sse2.UnpackLow(vecNarrow, zero).AsUInt64(); + var vecNarrow = Sse2.X64.ConvertScalarToVector128Int64(value).AsSByte(); + var vecWide = Sse2.UnpackLow(vecNarrow, zero).AsUInt64(); Sse2.Store((ulong*)output, vecWide); } else @@ -570,8 +570,8 @@ private static unsafe void WidenFourAsciiBytesToUtf16AndWriteToBuffer(char* outp // BMI2 could be used, but this variant is faster on both Intel and AMD. if (Sse2.X64.IsSupported) { - Vector128 vecNarrow = Sse2.ConvertScalarToVector128Int32(value).AsSByte(); - Vector128 vecWide = Sse2.UnpackLow(vecNarrow, zero).AsUInt64(); + var vecNarrow = Sse2.ConvertScalarToVector128Int32(value).AsSByte(); + var vecWide = Sse2.UnpackLow(vecNarrow, zero).AsUInt64(); Unsafe.WriteUnaligned(output, Sse2.X64.ConvertToUInt64(vecWide)); } else @@ -598,8 +598,8 @@ private static bool WidenFourAsciiBytesToUtf16AndCompareToChars(ref char charSta // BMI2 could be used, but this variant is faster on both Intel and AMD. if (Sse2.X64.IsSupported) { - Vector128 vecNarrow = Sse2.ConvertScalarToVector128UInt32(value).AsByte(); - Vector128 vecWide = Sse2.UnpackLow(vecNarrow, Vector128.Zero).AsUInt64(); + var vecNarrow = Sse2.ConvertScalarToVector128UInt32(value).AsByte(); + var vecWide = Sse2.UnpackLow(vecNarrow, Vector128.Zero).AsUInt64(); return Unsafe.ReadUnaligned(ref Unsafe.As(ref charStart)) == Sse2.X64.ConvertToUInt64(vecWide); } @@ -637,8 +637,8 @@ private static bool WidenTwoAsciiBytesToUtf16AndCompareToChars(ref char charStar // BMI2 could be used, but this variant is faster on both Intel and AMD. if (Sse2.IsSupported) { - Vector128 vecNarrow = Sse2.ConvertScalarToVector128UInt32(value).AsByte(); - Vector128 vecWide = Sse2.UnpackLow(vecNarrow, Vector128.Zero).AsUInt32(); + var vecNarrow = Sse2.ConvertScalarToVector128UInt32(value).AsByte(); + var vecWide = Sse2.UnpackLow(vecNarrow, Vector128.Zero).AsUInt32(); return Unsafe.ReadUnaligned(ref Unsafe.As(ref charStart)) == Sse2.ConvertToUInt32(vecWide); } @@ -725,34 +725,27 @@ private static void PopulateSpanWithHexSuffix(Span buffer, (string? str, c if (Ssse3.IsSupported) { - // These must be explicity typed as ReadOnlySpan - // They then become a non-allocating mappings to the data section of the assembly. - // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static - ReadOnlySpan shuffleMaskData = new byte[16] - { + // The constant inline vectors are read from the data section without any additional + // moves. See https://github.com/dotnet/runtime/issues/44115 Case 1.1 for further details. + + var lowNibbles = Ssse3.Shuffle(Vector128.CreateScalarUnsafe(tupleNumber).AsByte(), Vector128.Create( 0xF, 0xF, 3, 0xF, 0xF, 0xF, 2, 0xF, 0xF, 0xF, 1, 0xF, 0xF, 0xF, 0, 0xF - }; + ).AsByte()); - ReadOnlySpan asciiUpperCaseData = new byte[16] - { + var highNibbles = Sse2.ShiftRightLogical(Sse2.ShiftRightLogical128BitLane(lowNibbles, 2).AsInt32(), 4).AsByte(); + var indices = Sse2.And(Sse2.Or(lowNibbles, highNibbles), Vector128.Create((byte)0xF)); + + // Lookup the hex values at the positions of the indices + var hex = Ssse3.Shuffle(Vector128.Create( (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F' - }; - - // Load from data section memory into Vector128 registers - var shuffleMask = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetReference(shuffleMaskData)); - var asciiUpperCase = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetReference(asciiUpperCaseData)); + ), indices); - var lowNibbles = Ssse3.Shuffle(Vector128.CreateScalarUnsafe(tupleNumber).AsByte(), shuffleMask); - var highNibbles = Sse2.ShiftRightLogical(Sse2.ShiftRightLogical128BitLane(lowNibbles, 2).AsInt32(), 4).AsByte(); - var indices = Sse2.And(Sse2.Or(lowNibbles, highNibbles), Vector128.Create((byte)0xF)); - // Lookup the hex values at the positions of the indices - var hex = Ssse3.Shuffle(asciiUpperCase, indices); // The high bytes (0x00) of the chars have also been converted to ascii hex '0', so clear them out. hex = Sse2.And(hex, Vector128.Create((ushort)0xFF).AsByte()); From 709f6915887d377a04a7574235baabcb50eb8552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20S=C5=82owik?= Date: Fri, 2 Jul 2021 18:58:41 +0200 Subject: [PATCH 02/15] Fixed wrong property being used to log startup timeout in ReactDevelopmentServerMiddleware (#33998) --- .../ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/Spa/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/Spa/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs index 3876fe23d922..59248e271ebb 100644 --- a/src/Middleware/Spa/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/Spa/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs @@ -55,7 +55,7 @@ public static void Attach( // the first request times out, subsequent requests could still work. var timeout = spaBuilder.Options.StartupTimeout; var port = await portTask.WithTimeout(timeout, $"The create-react-app server did not start listening for requests " + - $"within the timeout period of {timeout.Seconds} seconds. " + + $"within the timeout period of {timeout.TotalSeconds} seconds. " + $"Check the log output for error information."); // Everything we proxy is hardcoded to target http://localhost because: From f3ae70fdbbd83fa3ccd7d073f3861dafc38db490 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 2 Jul 2021 10:26:03 -0700 Subject: [PATCH 03/15] Cleanup IDE errors in SignalR (#33918) --- .editorconfig | 3 +++ src/Shared/ValueStopwatch/ValueStopwatch.cs | 2 +- .../src/Internal/DefaultRetryPolicy.cs | 2 +- .../src/Internal/WebSocketsTransport.cs | 2 +- .../src/NegotiateProtocol.cs | 18 ++++++++--------- .../src/Internal/HttpConnectionContext.cs | 2 +- .../src/LongPollingOptions.cs | 3 +++ .../src/Protocol/JsonHubProtocol.cs | 20 +++++++++---------- .../common/Shared/AsyncEnumerableAdapters.cs | 4 ++-- src/SignalR/common/Shared/DuplexPipe.cs | 5 ++++- .../Protocol/StreamBindingFailureMessage.cs | 5 ++++- .../server/Core/src/HubConnectionContext.cs | 4 ++-- .../Core/src/Internal/HubConnectionBinder.cs | 8 ++++---- src/SignalR/server/Core/src/StreamTracker.cs | 4 ++-- 14 files changed, 47 insertions(+), 35 deletions(-) diff --git a/.editorconfig b/.editorconfig index 931e7898df78..ade15f781f50 100644 --- a/.editorconfig +++ b/.editorconfig @@ -244,6 +244,9 @@ dotnet_diagnostic.CA1846.severity = suggestion dotnet_diagnostic.CA2008.severity = suggestion # CA2012: Use ValueTask correctly dotnet_diagnostic.CA2012.severity = suggestion +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = suggestion + # CA2016: Forward the 'CancellationToken' parameter to methods that take one dotnet_diagnostic.CA2016.severity = suggestion diff --git a/src/Shared/ValueStopwatch/ValueStopwatch.cs b/src/Shared/ValueStopwatch/ValueStopwatch.cs index f99a084aebe0..0b27a299e682 100644 --- a/src/Shared/ValueStopwatch/ValueStopwatch.cs +++ b/src/Shared/ValueStopwatch/ValueStopwatch.cs @@ -10,7 +10,7 @@ internal struct ValueStopwatch { private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - private long _startTimestamp; + private readonly long _startTimestamp; public bool IsActive => _startTimestamp != 0; diff --git a/src/SignalR/clients/csharp/Client.Core/src/Internal/DefaultRetryPolicy.cs b/src/SignalR/clients/csharp/Client.Core/src/Internal/DefaultRetryPolicy.cs index 2c6573cc443a..392025b2da20 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/Internal/DefaultRetryPolicy.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/Internal/DefaultRetryPolicy.cs @@ -16,7 +16,7 @@ internal class DefaultRetryPolicy : IRetryPolicy null, }; - private TimeSpan?[] _retryDelays; + private readonly TimeSpan?[] _retryDelays; public DefaultRetryPolicy() { diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs index b6708645237d..64168e99151c 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs @@ -23,7 +23,7 @@ internal partial class WebSocketsTransport : ITransport private readonly ILogger _logger; private readonly TimeSpan _closeTimeout; private volatile bool _aborted; - private HttpConnectionOptions _httpConnectionOptions; + private readonly HttpConnectionOptions _httpConnectionOptions; private IDuplexPipe? _transport; diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs index a702606cd481..242806f123ad 100644 --- a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs +++ b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs @@ -17,23 +17,23 @@ namespace Microsoft.AspNetCore.Http.Connections public static class NegotiateProtocol { private const string ConnectionIdPropertyName = "connectionId"; - private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName); + private static readonly JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName); private const string ConnectionTokenPropertyName = "connectionToken"; - private static JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName); + private static readonly JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName); private const string UrlPropertyName = "url"; - private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName); + private static readonly JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName); private const string AccessTokenPropertyName = "accessToken"; - private static JsonEncodedText AccessTokenPropertyNameBytes = JsonEncodedText.Encode(AccessTokenPropertyName); + private static readonly JsonEncodedText AccessTokenPropertyNameBytes = JsonEncodedText.Encode(AccessTokenPropertyName); private const string AvailableTransportsPropertyName = "availableTransports"; - private static JsonEncodedText AvailableTransportsPropertyNameBytes = JsonEncodedText.Encode(AvailableTransportsPropertyName); + private static readonly JsonEncodedText AvailableTransportsPropertyNameBytes = JsonEncodedText.Encode(AvailableTransportsPropertyName); private const string TransportPropertyName = "transport"; - private static JsonEncodedText TransportPropertyNameBytes = JsonEncodedText.Encode(TransportPropertyName); + private static readonly JsonEncodedText TransportPropertyNameBytes = JsonEncodedText.Encode(TransportPropertyName); private const string TransferFormatsPropertyName = "transferFormats"; - private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName); + private static readonly JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName); private const string ErrorPropertyName = "error"; - private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName); + private static readonly JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName); private const string NegotiateVersionPropertyName = "negotiateVersion"; - private static JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName); + private static readonly JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName); // Use C#7.3's ReadOnlySpan optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/ // Used to detect ASP.NET SignalR Server connection attempt diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs index e5ed63f59474..f47b58fb504b 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs @@ -42,7 +42,7 @@ internal class HttpConnectionContext : ConnectionContext, private PipeWriterStream _applicationStream; private IDuplexPipe _application; private IDictionary? _items; - private CancellationTokenSource _connectionClosedTokenSource; + private readonly CancellationTokenSource _connectionClosedTokenSource; private CancellationTokenSource? _sendCts; private bool _activeSend; diff --git a/src/SignalR/common/Http.Connections/src/LongPollingOptions.cs b/src/SignalR/common/Http.Connections/src/LongPollingOptions.cs index 0ddc3ee12ac7..24867fdc5f2c 100644 --- a/src/SignalR/common/Http.Connections/src/LongPollingOptions.cs +++ b/src/SignalR/common/Http.Connections/src/LongPollingOptions.cs @@ -1,3 +1,6 @@ +// 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 System; namespace Microsoft.AspNetCore.Http.Connections diff --git a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs index 3569cfa8c9d5..54c33bb09a5c 100644 --- a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs +++ b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs @@ -23,25 +23,25 @@ namespace Microsoft.AspNetCore.SignalR.Protocol public sealed class JsonHubProtocol : IHubProtocol { private const string ResultPropertyName = "result"; - private static JsonEncodedText ResultPropertyNameBytes = JsonEncodedText.Encode(ResultPropertyName); + private static readonly JsonEncodedText ResultPropertyNameBytes = JsonEncodedText.Encode(ResultPropertyName); private const string ItemPropertyName = "item"; - private static JsonEncodedText ItemPropertyNameBytes = JsonEncodedText.Encode(ItemPropertyName); + private static readonly JsonEncodedText ItemPropertyNameBytes = JsonEncodedText.Encode(ItemPropertyName); private const string InvocationIdPropertyName = "invocationId"; - private static JsonEncodedText InvocationIdPropertyNameBytes = JsonEncodedText.Encode(InvocationIdPropertyName); + private static readonly JsonEncodedText InvocationIdPropertyNameBytes = JsonEncodedText.Encode(InvocationIdPropertyName); private const string StreamIdsPropertyName = "streamIds"; - private static JsonEncodedText StreamIdsPropertyNameBytes = JsonEncodedText.Encode(StreamIdsPropertyName); + private static readonly JsonEncodedText StreamIdsPropertyNameBytes = JsonEncodedText.Encode(StreamIdsPropertyName); private const string TypePropertyName = "type"; - private static JsonEncodedText TypePropertyNameBytes = JsonEncodedText.Encode(TypePropertyName); + private static readonly JsonEncodedText TypePropertyNameBytes = JsonEncodedText.Encode(TypePropertyName); private const string ErrorPropertyName = "error"; - private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName); + private static readonly JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName); private const string AllowReconnectPropertyName = "allowReconnect"; - private static JsonEncodedText AllowReconnectPropertyNameBytes = JsonEncodedText.Encode(AllowReconnectPropertyName); + private static readonly JsonEncodedText AllowReconnectPropertyNameBytes = JsonEncodedText.Encode(AllowReconnectPropertyName); private const string TargetPropertyName = "target"; - private static JsonEncodedText TargetPropertyNameBytes = JsonEncodedText.Encode(TargetPropertyName); + private static readonly JsonEncodedText TargetPropertyNameBytes = JsonEncodedText.Encode(TargetPropertyName); private const string ArgumentsPropertyName = "arguments"; - private static JsonEncodedText ArgumentsPropertyNameBytes = JsonEncodedText.Encode(ArgumentsPropertyName); + private static readonly JsonEncodedText ArgumentsPropertyNameBytes = JsonEncodedText.Encode(ArgumentsPropertyName); private const string HeadersPropertyName = "headers"; - private static JsonEncodedText HeadersPropertyNameBytes = JsonEncodedText.Encode(HeadersPropertyName); + private static readonly JsonEncodedText HeadersPropertyNameBytes = JsonEncodedText.Encode(HeadersPropertyName); private const string ProtocolName = "json"; private const int ProtocolVersion = 1; diff --git a/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs b/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs index a94f23296a07..44f0cd226b63 100644 --- a/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs +++ b/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs @@ -92,7 +92,7 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellati private class CancelableEnumerator : IAsyncEnumerator { - private IAsyncEnumerator _asyncEnumerator; + private readonly IAsyncEnumerator _asyncEnumerator; private readonly CancellationTokenRegistration _cancellationTokenRegistration; public T Current => (T)_asyncEnumerator.Current; @@ -118,7 +118,7 @@ public ValueTask DisposeAsync() private class BoxedAsyncEnumerator : IAsyncEnumerator { - private IAsyncEnumerator _asyncEnumerator; + private readonly IAsyncEnumerator _asyncEnumerator; public BoxedAsyncEnumerator(IAsyncEnumerator asyncEnumerator) { diff --git a/src/SignalR/common/Shared/DuplexPipe.cs b/src/SignalR/common/Shared/DuplexPipe.cs index 4fc01d01f53f..375131f835de 100644 --- a/src/SignalR/common/Shared/DuplexPipe.cs +++ b/src/SignalR/common/Shared/DuplexPipe.cs @@ -1,3 +1,6 @@ +// 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 System.Buffers; namespace System.IO.Pipelines @@ -38,4 +41,4 @@ public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) } } } -} \ No newline at end of file +} diff --git a/src/SignalR/common/SignalR.Common/src/Protocol/StreamBindingFailureMessage.cs b/src/SignalR/common/SignalR.Common/src/Protocol/StreamBindingFailureMessage.cs index 571e1fdc39fc..726e3dcf1fc0 100644 --- a/src/SignalR/common/SignalR.Common/src/Protocol/StreamBindingFailureMessage.cs +++ b/src/SignalR/common/SignalR.Common/src/Protocol/StreamBindingFailureMessage.cs @@ -1,4 +1,7 @@ -using System; +// 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 System; using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Text; diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index dcb55cab3dd5..f68920696cdc 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -46,8 +46,8 @@ public partial class HubConnectionContext private bool _clientTimeoutActive; private volatile bool _connectionAborted; private volatile bool _allowReconnect = true; - private int _streamBufferCapacity; - private long? _maxMessageSize; + private readonly int _streamBufferCapacity; + private readonly long? _maxMessageSize; private bool _receivedMessageTimeoutEnabled; private long _receivedMessageElapsedTicks; private long _receivedMessageTimestamp; diff --git a/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs b/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs index dcd4bd5c7d59..6a4c43562af5 100644 --- a/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs +++ b/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -9,8 +9,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal { internal class HubConnectionBinder : IInvocationBinder where THub : Hub { - private HubDispatcher _dispatcher; - private HubConnectionContext _connection; + private readonly HubDispatcher _dispatcher; + private readonly HubConnectionContext _connection; public HubConnectionBinder(HubDispatcher dispatcher, HubConnectionContext connection) { @@ -33,4 +33,4 @@ public Type GetStreamItemType(string streamId) return _connection.StreamTracker.GetStreamItemType(streamId); } } -} \ No newline at end of file +} diff --git a/src/SignalR/server/Core/src/StreamTracker.cs b/src/SignalR/server/Core/src/StreamTracker.cs index 9c329cfc051d..3ca2c7afe436 100644 --- a/src/SignalR/server/Core/src/StreamTracker.cs +++ b/src/SignalR/server/Core/src/StreamTracker.cs @@ -17,7 +17,7 @@ internal class StreamTracker { private static readonly MethodInfo _buildConverterMethod = typeof(StreamTracker).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Single(m => m.Name.Equals("BuildStream")); private readonly object[] _streamConverterArgs; - private ConcurrentDictionary _lookup = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _lookup = new ConcurrentDictionary(); public StreamTracker(int streamBufferCapacity) { @@ -100,7 +100,7 @@ private interface IStreamConverter private class ChannelConverter : IStreamConverter { - private Channel _channel; + private readonly Channel _channel; public ChannelConverter(int streamBufferCapacity) { From b444d8eb1547cff83420f63562d2e1c79991755e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 2 Jul 2021 11:18:46 -0700 Subject: [PATCH 04/15] Apply diagnostic and fix for API1000 and API1001 to expression-bodied methods (#34020) Fixes #33091 Co-authored-by: David Nelson --- .../src/ActualApiResponseMetadata.cs | 14 +- .../src/ActualApiResponseMetadataFactory.cs | 231 ++++++++++-------- .../AddResponseTypeAttributeCodeFixAction.cs | 11 +- ...AddResponseTypeAttributeCodeFixProvider.cs | 2 +- ...ireExplicitModelValidationCheckAnalyzer.cs | 10 +- .../src/ApiConventionAnalyzer.cs | 37 ++- .../ActualApiResponseMetadataFactoryTest.cs | 55 +++-- ...AttributeCodeFixProviderIntegrationTest.cs | 7 +- ...lValidationCheckAnalyzerIntegrationTest.cs | 2 +- ...ModelValidationCheckCodeFixProviderTest.cs | 2 +- .../ApiConventionAnalyzerIntegrationTest.cs | 2 +- .../test/DeclaredApiResponseMetadataTest.cs | 25 +- .../test/Mvc.Api.Analyzers.Test.csproj | 4 +- .../TryGetActualResponseMetadataTests.cs | 4 +- ...eFixWorksOnExpressionBodiedMethod.Input.cs | 9 + ...FixWorksOnExpressionBodiedMethod.Output.cs | 13 + ...tOfTReturningMethodWithoutAnyAttributes.cs | 2 +- ...OfTReturningMethodWithoutSomeAttributes.cs | 2 +- ...urned_ForControllerWithCustomConvention.cs | 2 +- ...Attribute_ReturnsUndocumentedStatusCode.cs | 2 +- ...Attribute_ReturnsUndocumentedStatusCode.cs | 2 +- ...ionMethod_ReturnsUndocumentedStatusCode.cs | 2 +- ...nouslyReturnsValue_WithoutDocumentation.cs | 2 +- ...ributeReturnsValue_WithoutDocumentation.cs | 2 +- ...fMethodWithAttribute_ReturnsDerivedType.cs | 2 +- ...onvention_ReturnsUndocumentedStatusCode.cs | 2 +- ...Attribute_ReturnsUndocumentedStatusCode.cs | 2 +- src/submodules/googletest | 2 +- 28 files changed, 258 insertions(+), 194 deletions(-) create mode 100644 src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Input.cs create mode 100644 src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Output.cs diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs index 2fdc277a0819..271f62f2cd0c 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs @@ -1,9 +1,9 @@ -// 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. using System; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { @@ -11,21 +11,21 @@ internal readonly struct ActualApiResponseMetadata { private readonly int? _statusCode; - public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, ITypeSymbol returnType) + public ActualApiResponseMetadata(IReturnOperation returnExpression, ITypeSymbol returnType) { - ReturnStatement = returnStatement; + ReturnOperation = returnExpression; ReturnType = returnType; _statusCode = null; } - public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode, ITypeSymbol? returnType) + public ActualApiResponseMetadata(IReturnOperation returnExpression, int statusCode, ITypeSymbol? returnType) { - ReturnStatement = returnStatement; + ReturnOperation = returnExpression; _statusCode = statusCode; ReturnType = returnType; } - public ReturnStatementSyntax ReturnStatement { get; } + public IReturnOperation ReturnOperation { get; } public int StatusCode => _statusCode ?? throw new ArgumentException("Status code is not available when IsDefaultResponse is true"); diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs index bfd7623c9e8e..e83c9efe1166 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs @@ -1,20 +1,20 @@ -// 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. -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FlowAnalysis; +using Microsoft.CodeAnalysis.Operations; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { public static class ActualApiResponseMetadataFactory { - private static readonly Func _shouldDescendIntoChildren = ShouldDescendIntoChildren; - /// /// This method looks at individual return statments and attempts to parse the status code and the return type. /// Given a for an action, this method inspects return statements in the body. @@ -24,33 +24,24 @@ public static class ActualApiResponseMetadataFactory /// internal static bool TryGetActualResponseMetadata( in ApiControllerSymbolCache symbolCache, - SemanticModel semanticModel, - MethodDeclarationSyntax methodSyntax, + IMethodBodyBaseOperation methodBody, CancellationToken cancellationToken, out IList actualResponseMetadata) { - actualResponseMetadata = new List(); + var localActualResponseMetadata = new List(); + var localSymbolCache = symbolCache; var allReturnStatementsReadable = true; - foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType()) + void AnalyzeResponseExpression(IReturnOperation returnOperation) { - if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression == null || returnStatementSyntax.Expression.IsMissing) - { - // Ignore malformed return statements. - allReturnStatementsReadable = false; - continue; - } + var responseMetadata = InspectReturnOperation( + localSymbolCache, + returnOperation); - var responseMetadata = InspectReturnStatementSyntax( - symbolCache, - semanticModel, - returnStatementSyntax, - cancellationToken); - - if (responseMetadata != null) + if (responseMetadata is { } value) { - actualResponseMetadata.Add(responseMetadata.Value); + localActualResponseMetadata.Add(value); } else { @@ -58,28 +49,38 @@ internal static bool TryGetActualResponseMetadata( } } + foreach (var operation in GetReturnStatements(methodBody)) + { + AnalyzeResponseExpression(operation); + } + + actualResponseMetadata = localActualResponseMetadata; return allReturnStatementsReadable; } - internal static ActualApiResponseMetadata? InspectReturnStatementSyntax( + internal static ActualApiResponseMetadata? InspectReturnOperation( in ApiControllerSymbolCache symbolCache, - SemanticModel semanticModel, - ReturnStatementSyntax returnStatementSyntax, - CancellationToken cancellationToken) + IReturnOperation returnOperation) { - var returnExpression = returnStatementSyntax.Expression; - var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken); - if (typeInfo.Type == null || typeInfo.Type.TypeKind == TypeKind.Error) + var returnedValue = returnOperation.ReturnedValue; + if (returnedValue is null || returnedValue is IInvalidOperation) { return null; } - var statementReturnType = typeInfo.Type; + // Covers conversion in the `IActionResult GetResult => NotFound()` case. + // Multiple conversions can happen for ActionResult, hence a while loop. + while (returnedValue is IConversionOperation conversion) + { + returnedValue = conversion.Operand; + } + + var statementReturnType = returnedValue.Type; if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType)) { // Return expression is not an instance of IActionResult. Must be returning the "model". - return new ActualApiResponseMetadata(returnStatementSyntax, statementReturnType); + return new ActualApiResponseMetadata(returnOperation, statementReturnType); } var defaultStatusCodeAttribute = statementReturnType @@ -87,169 +88,158 @@ internal static bool TryGetActualResponseMetadata( .FirstOrDefault(); var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute); + ITypeSymbol? returnType = null; - switch (returnExpression) + switch (returnedValue) { - case InvocationExpressionSyntax invocation: + case IInvocationOperation invocation: { // Covers the 'return StatusCode(200)' case. - var result = InspectMethodArguments(semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken); + var result = InspectMethodArguments(invocation.Arguments); statusCode = result.statusCode ?? statusCode; returnType = result.returnType; break; } - case ObjectCreationExpressionSyntax creation: + case IObjectCreationOperation creation: { // Read values from 'return new StatusCodeResult(200) case. - var result = InspectMethodArguments(semanticModel, creation, creation.ArgumentList, cancellationToken); + var result = InspectMethodArguments(creation.Arguments); statusCode = result.statusCode ?? statusCode; returnType = result.returnType; // Read values from property assignments e.g. 'return new ObjectResult(...) { StatusCode = 200 }'. // Property assignments override constructor assigned values and defaults. - result = InspectInitializers(symbolCache, semanticModel, creation.Initializer, cancellationToken); - statusCode = result.statusCode ?? statusCode; - returnType = result.returnType ?? returnType; + if (creation.Initializer is not null) + { + + result = InspectInitializers(symbolCache, creation.Initializer); + statusCode = result.statusCode ?? statusCode; + returnType = result.returnType ?? returnType; + } break; } } + if (statusCode == null) { return null; } - return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value, returnType); + return new ActualApiResponseMetadata(returnOperation, statusCode.Value, returnType); } private static (int? statusCode, ITypeSymbol? returnType) InspectInitializers( in ApiControllerSymbolCache symbolCache, - SemanticModel semanticModel, - InitializerExpressionSyntax? initializer, - CancellationToken cancellationToken) + IObjectOrCollectionInitializerOperation initializer) { int? statusCode = null; ITypeSymbol? typeSymbol = null; - for (var i = 0; initializer != null && i < initializer.Expressions.Count; i++) + foreach (var child in initializer.Children) { - var expression = initializer.Expressions[i]; - - if (!(expression is AssignmentExpressionSyntax assignment) || - !(assignment.Left is IdentifierNameSyntax identifier)) + if (child is not IAssignmentOperation assignmentOperation || + assignmentOperation.Target is not IPropertyReferenceOperation propertyReference) { continue; } - var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken); - if (symbolInfo.Symbol is IPropertySymbol property) + var property = propertyReference.Property; + + if (IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty)) { - if (IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty) && - TryGetExpressionStatusCode(semanticModel, assignment.Right, cancellationToken, out var statusCodeValue)) + // Look for assignments to IStatusCodeActionResult.StatusCode + if (TryGetStatusCode(assignmentOperation.Value, out var statusCodeValue)) { - // Look for assignments to IStatusCodeActionResult.StatusCode + // new StatusCodeResult { StatusCode = someLocal }; statusCode = statusCodeValue; } - else if (HasAttributeNamed(property, ApiSymbolNames.ActionResultObjectValueAttribute)) - { - // Look for assignment to a property annotated with [ActionResultObjectValue] - typeSymbol = GetExpressionObjectType(semanticModel, assignment.Right, cancellationToken); - } + } + else if (HasAttributeNamed(property, ApiSymbolNames.ActionResultObjectValueAttribute)) + { + // Look for assignment to a property annotated with [ActionResultObjectValue] + typeSymbol = assignmentOperation.Type; } } return (statusCode, typeSymbol); } - private static (int? statusCode, ITypeSymbol? returnType) InspectMethodArguments( - SemanticModel semanticModel, - ExpressionSyntax expression, - BaseArgumentListSyntax argumentList, - CancellationToken cancellationToken) + private static (int? statusCode, ITypeSymbol? returnType) InspectMethodArguments(ImmutableArray arguments) { int? statusCode = null; ITypeSymbol? typeSymbol = null; - var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); - - if (symbolInfo.Symbol is IMethodSymbol method) + foreach (var argument in arguments) { - for (var i = 0; i < method.Parameters.Length; i++) + var parameter = argument.Parameter; + if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultStatusCodeAttribute)) { - var parameter = method.Parameters[i]; - if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultStatusCodeAttribute)) + if (TryGetStatusCode(argument.Value, out var statusCodeValue)) { - var argument = argumentList.Arguments[parameter.Ordinal]; - if (TryGetExpressionStatusCode(semanticModel, argument.Expression, cancellationToken, out var statusCodeValue)) - { - statusCode = statusCodeValue; - } + statusCode = statusCodeValue; } + } - if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultObjectValueAttribute)) + if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultObjectValueAttribute)) + { + var operation = argument.Value; + + if (operation is IConversionOperation conversionOperation) { - var argument = argumentList.Arguments[parameter.Ordinal]; - typeSymbol = GetExpressionObjectType(semanticModel, argument.Expression, cancellationToken); + // new BadRequest((object)MyDataType); + operation = conversionOperation.Operand; } + + typeSymbol = operation.Type; } } return (statusCode, typeSymbol); } - private static ITypeSymbol? GetExpressionObjectType(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) - { - var typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken); - - return typeInfo.Type; - } - - private static bool TryGetExpressionStatusCode( - SemanticModel semanticModel, - ExpressionSyntax expression, - CancellationToken cancellationToken, + private static bool TryGetStatusCode( + IOperation operation, out int statusCode) { - if (expression is LiteralExpressionSyntax literal && literal.Token.Value is int literalStatusCode) + if (operation is IConversionOperation conversion) + { + // Could be an implicit conversation from int -> int? + operation = conversion.Operand; + } + + if (operation.ConstantValue is { HasValue: true } constant) { // Covers the 'return StatusCode(200)' case. - statusCode = literalStatusCode; + statusCode = (int)constant.Value; return true; } - if (expression is IdentifierNameSyntax || expression is MemberAccessExpressionSyntax) + if (operation is IMemberReferenceOperation memberReference) { - var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); - - if (symbolInfo.Symbol is IFieldSymbol field && field.HasConstantValue && field.ConstantValue is int constantStatusCode) + if (memberReference.Member is IFieldSymbol field && field.HasConstantValue && field.ConstantValue is int constantStatusCode) { // Covers the 'return StatusCode(StatusCodes.Status200OK)' case. // It also covers the 'return StatusCode(StatusCode)' case, where 'StatusCode' is a constant field. statusCode = constantStatusCode; return true; } - - if (symbolInfo.Symbol is ILocalSymbol local && local.HasConstantValue && local.ConstantValue is int localStatusCode) + } + else if (operation is ILocalReferenceOperation localReference) + { + if (localReference.ConstantValue is { HasValue: true } localConstant) { // Covers the 'return StatusCode(statusCode)' case, where 'statusCode' is a local constant. - statusCode = localStatusCode; + statusCode = (int)localConstant.Value; return true; } } - statusCode = default; + statusCode = 0; return false; } - private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode) - { - return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) && - !syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) && - !syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) && - !syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression); - } - internal static int? GetDefaultStatusCode(AttributeData attribute) { if (attribute != null && @@ -296,5 +286,34 @@ private static bool HasAttributeNamed(ISymbol symbol, string attributeName) return false; } + + private static IEnumerable GetReturnStatements(IMethodBodyBaseOperation method) + { + foreach (var returnOperation in method.Descendants().OfType()) + { + if (!AncestorIsLocalFunction(returnOperation)) + { + yield return returnOperation; + } + } + + bool AncestorIsLocalFunction(IReturnOperation operation) + { + var parent = operation.Parent; + while (parent != method) + { + if (parent is ILocalFunctionOperation or IAnonymousFunctionOperation) + { + return true; + } + + + parent = parent.Parent; + } + + return false; + } + } + } } diff --git a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs index b2f0428bf57f..10dde8a026e8 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Simplification; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers @@ -128,8 +129,8 @@ protected override async Task GetChangedDocumentAsync(CancellationToke { var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var methodReturnStatement = (ReturnStatementSyntax)root.FindNode(_diagnostic.Location.SourceSpan); - var methodSyntax = methodReturnStatement.FirstAncestorOrSelf(); + var diagnosticNode = root.FindNode(_diagnostic.Location.SourceSpan); + var methodSyntax = diagnosticNode.FirstAncestorOrSelf(); var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken); var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes); @@ -168,7 +169,9 @@ private static Dictionary GetStatusCodeConstants(INamedTypeSymbol s private ICollection<(int statusCode, ITypeSymbol? typeSymbol)> CalculateStatusCodesToApply(in CodeActionContext context, IList declaredResponseMetadata) { - if (!ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata)) + var operation = (IMethodBodyBaseOperation)context.SemanticModel.GetOperation(context.MethodSyntax, context.CancellationToken); + + if (!ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(context.SymbolCache, operation, context.CancellationToken, out var actualResponseMetadata)) { // If we cannot parse metadata correctly, don't offer fixes. return Array.Empty<(int, ITypeSymbol?)>(); diff --git a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixProvider.cs b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixProvider.cs index bd9734e0b2ad..c42b3c0638bc 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixProvider.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixProvider.cs @@ -1,4 +1,4 @@ -// 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. using System.Collections.Immutable; diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs b/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs index b1f52a8c78c0..bb79c18c1dc5 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs @@ -109,18 +109,16 @@ private void InitializeWorker(CompilationStartAnalysisContext context, ApiContro return; } - var returnStatementSyntax = (ReturnStatementSyntax)returnOperation.Syntax; - var actualMetadata = ActualApiResponseMetadataFactory.InspectReturnStatementSyntax( - symbolCache, - semanticModel, - returnStatementSyntax, - operationAnalysisContext.CancellationToken); + var actualMetadata = ActualApiResponseMetadataFactory.InspectReturnOperation( + in symbolCache, + returnOperation); if (actualMetadata == null || actualMetadata.Value.StatusCode != 400) { return; } + var returnStatementSyntax = returnOperation.Syntax; var additionalLocations = new[] { ifStatement.GetLocation(), diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs b/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs index 5890981a1060..224476bccb73 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs @@ -1,4 +1,4 @@ -// 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. using System.Collections.Generic; @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { @@ -37,44 +38,40 @@ public override void Initialize(AnalysisContext context) private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, ApiControllerSymbolCache symbolCache) { - compilationStartAnalysisContext.RegisterSyntaxNodeAction(syntaxNodeContext => + compilationStartAnalysisContext.RegisterOperationAction(operationStartContext => { - var cancellationToken = syntaxNodeContext.CancellationToken; - var methodSyntax = (MethodDeclarationSyntax)syntaxNodeContext.Node; - var semanticModel = syntaxNodeContext.SemanticModel; - var method = semanticModel.GetDeclaredSymbol(methodSyntax, syntaxNodeContext.CancellationToken); - + var method = (IMethodSymbol)operationStartContext.ContainingSymbol; if (!ApiControllerFacts.IsApiControllerAction(symbolCache, method)) { return; } var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); - var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata); + var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, (IMethodBodyOperation)operationStartContext.Operation, operationStartContext.CancellationToken, out var actualResponseMetadata); var hasUndocumentedStatusCodes = false; foreach (var actualMetadata in actualResponseMetadata) { - var location = actualMetadata.ReturnStatement.GetLocation(); + var location = actualMetadata.ReturnOperation.ReturnedValue.Syntax.GetLocation(); if (!DeclaredApiResponseMetadata.Contains(declaredResponseMetadata, actualMetadata)) { hasUndocumentedStatusCodes = true; if (actualMetadata.IsDefaultResponse) { - syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( + operationStartContext.ReportDiagnostic(Diagnostic.Create( ApiDiagnosticDescriptors.API1001_ActionReturnsUndocumentedSuccessResult, location)); } else - { - syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( - ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode, - location, - actualMetadata.StatusCode)); + { + operationStartContext.ReportDiagnostic(Diagnostic.Create( + ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode, + location, + actualMetadata.StatusCode)); + } } } - } if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes) { @@ -88,21 +85,21 @@ private void InitializeWorker(CompilationStartAnalysisContext compilationStartAn var declaredMetadata = declaredResponseMetadata[i]; if (!Contains(actualResponseMetadata, declaredMetadata)) { - syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( + operationStartContext.ReportDiagnostic(Diagnostic.Create( ApiDiagnosticDescriptors.API1002_ActionDoesNotReturnDocumentedStatusCode, - methodSyntax.Identifier.GetLocation(), + method.Locations[0], declaredMetadata.StatusCode)); } } - }, SyntaxKind.MethodDeclaration); + }, OperationKind.MethodBody); } internal static bool Contains(IList actualResponseMetadata, DeclaredApiResponseMetadata declaredMetadata) { for (var i = 0; i < actualResponseMetadata.Count; i++) { - if (declaredMetadata.Matches(actualResponseMetadata[i])) + if (declaredMetadata.Matches(actualResponseMetadata[i])) { return true; } diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs index cca12582dd9c..9199ec90e1ec 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs @@ -1,4 +1,4 @@ -// 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. using System.Collections.Generic; @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ActualApiResponseMetadataFactoryTest; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; using Xunit; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers @@ -74,12 +75,11 @@ public IActionResult Get(int id) var method = (IMethodSymbol)returnType.GetMembers().First(); var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); var returnStatement = methodSyntax.DescendantNodes().OfType().First(); + var returnOperation = (IReturnOperation)compilation.GetSemanticModel(syntaxTree).GetOperation(returnStatement); - var actualResponseMetadata = ActualApiResponseMetadataFactory.InspectReturnStatementSyntax( + var actualResponseMetadata = ActualApiResponseMetadataFactory.InspectReturnOperation( symbolCache, - compilation.GetSemanticModel(syntaxTree), - returnStatement, - CancellationToken.None); + returnOperation); // Assert Assert.Null(actualResponseMetadata); @@ -288,13 +288,34 @@ public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel() { Assert.False(metadata.IsDefaultResponse); Assert.Equal(204, metadata.StatusCode); - AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation()); + AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnOperation.Syntax.GetLocation()); }, metadata => { Assert.True(metadata.IsDefaultResponse); - AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation()); + AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnOperation.Syntax.GetLocation()); + }); + } + + [Fact] + public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResultExpression() + { + // Arrange + var typeName = typeof(TryGetActualResponseMetadataController).FullName; + var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResultExpression); + + // Act + var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName); + + // Assert + Assert.True(success); + Assert.Collection( + responseMetadatas, + metadata => + { + Assert.False(metadata.IsDefaultResponse); + Assert.Equal(200, metadata.StatusCode); }); } @@ -311,14 +332,14 @@ public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel() var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree; var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); - var semanticModel = compilation.GetSemanticModel(syntaxTree); + var methodOperation = (IMethodBodyBaseOperation)compilation.GetSemanticModel(syntaxTree).GetOperation(methodSyntax); - var result = ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas); + var result = ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, methodOperation, CancellationToken.None, out var responseMetadatas); return (result, responseMetadatas, testSource); } - private async Task RunInspectReturnStatementSyntax([CallerMemberName]string test = null) + private async Task RunInspectReturnStatementSyntax([CallerMemberName] string test = null) { // Arrange var compilation = await GetCompilation("InspectReturnExpressionTests"); @@ -330,12 +351,11 @@ public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel() var method = (IMethodSymbol)Assert.Single(controllerType.GetMembers(test)); var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); var returnStatement = methodSyntax.DescendantNodes().OfType().First(); + var returnOperation = (IReturnOperation)compilation.GetSemanticModel(syntaxTree).GetOperation(returnStatement); - return ActualApiResponseMetadataFactory.InspectReturnStatementSyntax( + return ActualApiResponseMetadataFactory.InspectReturnOperation( symbolCache, - compilation.GetSemanticModel(syntaxTree), - returnStatement, - CancellationToken.None); + returnOperation); } private async Task RunInspectReturnStatementSyntax(string source, string test) @@ -350,12 +370,11 @@ public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel() var method = (IMethodSymbol)returnType.GetMembers().First(); var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); var returnStatement = methodSyntax.DescendantNodes().OfType().First(); + var returnOperation = (IReturnOperation)compilation.GetSemanticModel(syntaxTree).GetOperation(returnStatement); - return ActualApiResponseMetadataFactory.InspectReturnStatementSyntax( + return ActualApiResponseMetadataFactory.InspectReturnOperation( symbolCache, - compilation.GetSemanticModel(syntaxTree), - returnStatement, - CancellationToken.None); + returnOperation); } private Task GetCompilation(string test) diff --git a/src/Mvc/Mvc.Api.Analyzers/test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs index 10a77b8b42a2..1f4aec10fca3 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs @@ -1,4 +1,4 @@ -// 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. using System.Runtime.CompilerServices; @@ -54,6 +54,9 @@ public class AddResponseTypeAttributeCodeFixProviderIntegrationTest [Fact] public Task CodeFixWorksWhenMultipleIdenticalStatusCodesAreInError() => RunTest(); + [Fact] + public Task CodeFixWorksOnExpressionBodiedMethod() => RunTest(); + private async Task RunTest([CallerMemberName] string testMethod = "") { // Arrange @@ -64,6 +67,8 @@ private async Task RunTest([CallerMemberName] string testMethod = "") // Act var diagnostics = await AnalyzerRunner.GetDiagnosticsAsync(project); + Assert.NotEmpty(diagnostics); + var actualOutput = await CodeFixRunner.ApplyCodeFixAsync( new AddResponseTypeAttributeCodeFixProvider(), project.GetDocument(controllerDocument), diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzerIntegrationTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzerIntegrationTest.cs index 9b3e54601469..e101450c4764 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzerIntegrationTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzerIntegrationTest.cs @@ -1,4 +1,4 @@ -// 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. using System.Runtime.CompilerServices; diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProviderTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProviderTest.cs index b8b096d9c947..486ad71a89be 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProviderTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProviderTest.cs @@ -1,4 +1,4 @@ -// 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. using System.Runtime.CompilerServices; diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs index ea640f424a9f..b5b2bd2c3d77 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs @@ -60,7 +60,7 @@ public IActionResult Get(int id) { if (id == 0) { - /*MM*/return NotFound(); + return /*MM*/NotFound(); } return; diff --git a/src/Mvc/Mvc.Api.Analyzers/test/DeclaredApiResponseMetadataTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/DeclaredApiResponseMetadataTest.cs index eee4a862f3e9..4dc929182634 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/DeclaredApiResponseMetadataTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/DeclaredApiResponseMetadataTest.cs @@ -1,11 +1,10 @@ -// 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. using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; using Moq; using Xunit; @@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { public class DeclaredApiResponseMetadataTest { - private readonly ReturnStatementSyntax ReturnStatement = SyntaxFactory.ReturnStatement(); + private readonly IReturnOperation ReturnExpression = Mock.Of(); private readonly AttributeData AttributeData = new TestAttributeData(); [Fact] @@ -21,7 +20,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataIs { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse; - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -35,7 +34,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataRe { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse; - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, 200, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -49,7 +48,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataIs200_AndActualMetadataIsDefau { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(200, AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -67,7 +66,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataIs201_AndActualMetadataIsDefau { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -85,7 +84,7 @@ public void Matches_ReturnsFalse_IfDeclaredMetadataIs201_AndActualMetadataIs200( { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, 200, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -99,7 +98,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataAndActualMetadataHaveSameStatu { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(302, AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, 302, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -116,7 +115,7 @@ public void Matches_ReturnsTrue_IfDeclaredMetadataIsDefault_AndActualMetadataIsE { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, actualStatusCode, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -130,7 +129,7 @@ public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIs { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, 204, null); // Act var matches = declaredMetadata.Matches(actualMetadata); @@ -144,7 +143,7 @@ public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIs { // Arrange var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of()); - var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null); + var actualMetadata = new ActualApiResponseMetadata(ReturnExpression, null); // Act var matches = declaredMetadata.Matches(actualMetadata); diff --git a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj index 3b4f947e80c9..7e6550066c56 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj +++ b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -8,7 +8,7 @@ - + diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ActualApiResponseMetadataFactoryTest/TryGetActualResponseMetadataTests.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ActualApiResponseMetadataFactoryTest/TryGetActualResponseMetadataTests.cs index 389b605adbd5..1a7c9b05a89a 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ActualApiResponseMetadataFactoryTest/TryGetActualResponseMetadataTests.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ActualApiResponseMetadataFactoryTest/TryGetActualResponseMetadataTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ActualApiResponseMetadataFactoryTest @@ -32,6 +32,8 @@ public async Task> ActionReturni /*MM2*/return new TryGetActualResponseMetadataModel(); } + + public IActionResult ActionWithActionResultOfTReturningOkResultExpression() => Ok(); } public class TryGetActualResponseMetadataModel { } diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Input.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Input.cs new file mode 100644 index 000000000000..463e5a9ce57f --- /dev/null +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Input.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWorksOnExpressionBodiedMethodController : ControllerBase + { + public IActionResult GetItem() => NotFound(); + } +} diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Output.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Output.cs new file mode 100644 index 000000000000..ef8ceb6c063d --- /dev/null +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWorksOnExpressionBodiedMethod.Output.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWorksOnExpressionBodiedMethodController : ControllerBase + { + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesDefaultResponseType] + public IActionResult GetItem() => NotFound(); + } +} diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes.cs index 42ce3bd06948..275941debea4 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes.cs @@ -12,7 +12,7 @@ public ActionResult Method(Guid? id) { if (id == null) { - /*MM*/return NotFound(); + return /*MM*/NotFound(); } return "Hello world"; diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes.cs index 1b341aed17b2..2769af3e56d0 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes.cs @@ -16,7 +16,7 @@ public IActionResult Put(int id, object model) if (!ModelState.IsValid) { - /*MM*/return UnprocessableEntity(); + return /*MM*/UnprocessableEntity(); } return Ok(); diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForControllerWithCustomConvention.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForControllerWithCustomConvention.cs index 884d613e0476..798f3cd7420f 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForControllerWithCustomConvention.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_ForControllerWithCustomConvention.cs @@ -14,7 +14,7 @@ public async Task Update(int id, Product product) { if (id < 0) { - /*MM*/return BadRequest(); + return /*MM*/BadRequest(); } try diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs index 3e263cffd926..94e988549362 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs @@ -15,7 +15,7 @@ public async ValueTask Method(int id) return NotFound(); } - /*MM*/return Ok(); + return /*MM*/Ok(); } } } diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs index d61842d16f63..2f43a890d256 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode.cs @@ -12,7 +12,7 @@ public async Task Method(int id) await Task.Yield(); if (id == 0) { - /*MM*/return NotFound(); + return /*MM*/NotFound(); } return Ok(); diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithApiConventionMethod_ReturnsUndocumentedStatusCode.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithApiConventionMethod_ReturnsUndocumentedStatusCode.cs index f09255b7d0f7..ef063d3004c7 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithApiConventionMethod_ReturnsUndocumentedStatusCode.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithApiConventionMethod_ReturnsUndocumentedStatusCode.cs @@ -10,7 +10,7 @@ public class DiagnosticsAreReturned_IfMethodWithApiConventionMethod_ReturnsUndoc [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))] public IActionResult Get(int id) { - /*MM*/return Accepted(); + return /*MM*/Accepted(); } } } diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation.cs index 4a93438cfd68..025cfd754060 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation.cs @@ -15,7 +15,7 @@ public async Task Date: Sat, 3 Jul 2021 09:50:32 +1200 Subject: [PATCH 05/15] HTTP/3: QUIC stream tests (#34023) --- .../Core/test/SniOptionsSelectorTests.cs | 2 +- .../Transport.Quic/src/Internal/IQuicTrace.cs | 4 + .../src/Internal/QuicConnectionContext.cs | 50 ++-- .../src/Internal/QuicConnectionListener.cs | 6 +- .../src/Internal/QuicStreamContext.cs | 18 +- .../Transport.Quic/src/Internal/QuicTrace.cs | 73 ++++- .../src/QuicConnectionFactory.cs | 1 + .../src/QuicTransportFactory.cs | 2 +- .../test/QuicConnectionContextTests.cs | 8 +- .../test/QuicConnectionListenerTests.cs | 6 +- .../test/QuicStreamContextTests.cs | 283 ++++++++++++++++++ .../Transport.Quic/test/QuicTestHelpers.cs | 15 +- .../test/QuicTransportFactoryTests.cs | 2 +- .../HttpsConnectionMiddlewareTests.cs | 4 +- 14 files changed, 418 insertions(+), 56 deletions(-) create mode 100644 src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs index 11605e628d91..8474a0dd3c04 100644 --- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs +++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class SniOptionsSelectorTests { - private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); + private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); [Fact] public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly() diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs index 555aa85780aa..3f94cc7427f8 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs @@ -11,11 +11,15 @@ internal interface IQuicTrace : ILogger { void AcceptedConnection(BaseConnectionContext connection); void AcceptedStream(QuicStreamContext streamContext); + void ConnectedStream(QuicStreamContext streamContext); void ConnectionError(BaseConnectionContext connection, Exception ex); + void ConnectionAborted(BaseConnectionContext connection, Exception ex); + void ConnectionAbort(BaseConnectionContext connection, string reason); void StreamError(QuicStreamContext streamContext, Exception ex); void StreamPause(QuicStreamContext streamContext); void StreamResume(QuicStreamContext streamContext); void StreamShutdownWrite(QuicStreamContext streamContext, string reason); + void StreamAborted(QuicStreamContext streamContext, Exception ex); void StreamAbort(QuicStreamContext streamContext, string reason); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 17a5a6dc20cb..99b4d13a99f3 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -31,24 +31,6 @@ public QuicConnectionContext(QuicConnection connection, QuicTransportContext con ConnectionClosed = _connectionClosedTokenSource.Token; Features.Set(new FakeTlsConnectionFeature()); Features.Set(this); - - _log.AcceptedConnection(this); - } - - public ValueTask StartUnidirectionalStreamAsync() - { - var stream = _connection.OpenUnidirectionalStream(); - var context = new QuicStreamContext(stream, this, _context); - context.Start(); - return new ValueTask(context); - } - - public ValueTask StartBidirectionalStreamAsync() - { - var stream = _connection.OpenBidirectionalStream(); - var context = new QuicStreamContext(stream, this, _context); - context.Start(); - return new ValueTask(context); } public override async ValueTask DisposeAsync() @@ -71,6 +53,7 @@ public override async ValueTask DisposeAsync() public override void Abort(ConnectionAbortedException abortReason) { // dedup calls to abort here. + _log.ConnectionAbort(this, abortReason.Message); _closeTask = _connection.CloseAsync(errorCode: Error).AsTask(); } @@ -81,13 +64,22 @@ public override void Abort(ConnectionAbortedException abortReason) var stream = await _connection.AcceptStreamAsync(cancellationToken); var context = new QuicStreamContext(stream, this, _context); context.Start(); + + _log.AcceptedStream(context); + return context; } catch (QuicConnectionAbortedException ex) { // Shutdown initiated by peer, abortive. - // TODO cancel CTS here? - _log.LogDebug($"Accept loop ended with exception: {ex.Message}"); + _log.ConnectionAborted(this, ex); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + state.CancelConnectionClosedToken(); + }, + this, + preferLocal: false); } catch (QuicOperationAbortedException) { @@ -99,13 +91,25 @@ public override void Abort(ConnectionAbortedException abortReason) return null; } + private void CancelConnectionClosedToken() + { + try + { + _connectionClosedTokenSource.Cancel(); + } + catch (Exception ex) + { + _log.LogError(0, ex, $"Unexpected exception in {nameof(QuicConnectionContext)}.{nameof(CancelConnectionClosedToken)}."); + } + } + public override ValueTask ConnectAsync(IFeatureCollection? features = null, CancellationToken cancellationToken = default) { QuicStream quicStream; - if (features != null) + var streamDirectionFeature = features?.Get(); + if (streamDirectionFeature != null) { - var streamDirectionFeature = features.Get()!; if (streamDirectionFeature.CanRead) { quicStream = _connection.OpenBidirectionalStream(); @@ -123,6 +127,8 @@ public override ValueTask ConnectAsync(IFeatureCollection? fe var context = new QuicStreamContext(quicStream, this, _context); context.Start(); + _log.ConnectedStream(context); + return new ValueTask(context); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index 6f9b6932cd6c..b90cad6ad69e 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -55,7 +55,11 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP try { var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken); - return new QuicConnectionContext(quicConnection, _context); + var connectionContext = new QuicConnectionContext(quicConnection, _context); + + _log.AcceptedConnection(connectionContext); + + return connectionContext; } catch (QuicOperationAbortedException ex) { diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index f65dfb910d4a..076965e44a87 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -16,7 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal { internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature { - private Task _processingTask = Task.CompletedTask; + // Internal for testing. + internal Task _processingTask = Task.CompletedTask; + private readonly QuicStream _stream; private readonly QuicConnectionContext _connection; private readonly QuicTransportContext _context; @@ -126,6 +128,8 @@ private async Task DoReceive() { // This could be ignored if _shutdownReason is already set. error = new ConnectionResetException(ex.Message, ex); + + _log.StreamAbort(this, error.Message); } catch (Exception ex) { @@ -233,7 +237,7 @@ private async Task DoSend() { shutdownReason = ex; unexpectedError = ex; - _log.ConnectionError(this, unexpectedError); + _log.StreamError(this, unexpectedError); } finally { @@ -294,8 +298,14 @@ public override void Abort(ConnectionAbortedException abortReason) lock (_shutdownLock) { - _stream.AbortRead(Error); - _stream.AbortWrite(Error); + if (_stream.CanRead) + { + _stream.AbortRead(Error); + } + if (_stream.CanWrite) + { + _stream.AbortWrite(Error); + } } // Cancel ProcessSends loop after calling shutdown to ensure the correct _shutdownReason gets set. diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs index 352688bfba5f..d3f0d5a85c2b 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs @@ -11,20 +11,28 @@ internal class QuicTrace : IQuicTrace { private static readonly Action _acceptedConnection = LoggerMessage.Define(LogLevel.Debug, new EventId(1, "AcceptedConnection"), @"Connection id ""{ConnectionId}"" accepted.", skipEnabledCheck: true); - private static readonly Action _acceptedStream = - LoggerMessage.Define(LogLevel.Debug, new EventId(2, "AcceptedStream"), @"Stream id ""{ConnectionId}"" accepted.", skipEnabledCheck: true); + private static readonly Action _acceptedStream = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "AcceptedStream"), @"Stream id ""{ConnectionId}"" type {StreamType} accepted.", skipEnabledCheck: true); + private static readonly Action _connectedStream = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ConnectedStream"), @"Stream id ""{ConnectionId}"" type {StreamType} connected.", skipEnabledCheck: true); private static readonly Action _connectionError = - LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ConnectionError"), @"Connection id ""{ConnectionId}"" unexpected error.", skipEnabledCheck: true); + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "ConnectionError"), @"Connection id ""{ConnectionId}"" unexpected error.", skipEnabledCheck: true); + private static readonly Action _connectionAborted = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ConnectionAborted"), @"Connection id ""{ConnectionId}"" aborted by peer.", skipEnabledCheck: true); + private static readonly Action _connectionAbort = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "ConnectionAbort"), @"Connection id ""{ConnectionId}"" aborted by application because: ""{Reason}"".", skipEnabledCheck: true); private static readonly Action _streamError = - LoggerMessage.Define(LogLevel.Debug, new EventId(4, "StreamError"), @"Stream id ""{ConnectionId}"" unexpected error.", skipEnabledCheck: true); + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "StreamError"), @"Stream id ""{ConnectionId}"" unexpected error.", skipEnabledCheck: true); private static readonly Action _streamPause = - LoggerMessage.Define(LogLevel.Debug, new EventId(5, "StreamPause"), @"Stream id ""{ConnectionId}"" paused.", skipEnabledCheck: true); + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "StreamPause"), @"Stream id ""{ConnectionId}"" paused.", skipEnabledCheck: true); private static readonly Action _streamResume = - LoggerMessage.Define(LogLevel.Debug, new EventId(6, "StreamResume"), @"Stream id ""{ConnectionId}"" resumed.", skipEnabledCheck: true); + LoggerMessage.Define(LogLevel.Debug, new EventId(9, "StreamResume"), @"Stream id ""{ConnectionId}"" resumed.", skipEnabledCheck: true); private static readonly Action _streamShutdownWrite = - LoggerMessage.Define(LogLevel.Debug, new EventId(7, "StreamShutdownWrite"), @"Stream id ""{ConnectionId}"" shutting down writes because: ""{Reason}"".", skipEnabledCheck: true); - private static readonly Action _streamAborted = - LoggerMessage.Define(LogLevel.Debug, new EventId(8, "StreamAbort"), @"Stream id ""{ConnectionId}"" aborted by application because: ""{Reason}"".", skipEnabledCheck: true); + LoggerMessage.Define(LogLevel.Debug, new EventId(10, "StreamShutdownWrite"), @"Stream id ""{ConnectionId}"" shutting down writes because: ""{Reason}"".", skipEnabledCheck: true); + private static readonly Action _streamAborted = + LoggerMessage.Define(LogLevel.Debug, new EventId(11, "StreamAborted"), @"Stream id ""{ConnectionId}"" aborted by peer.", skipEnabledCheck: true); + private static readonly Action _streamAbort = + LoggerMessage.Define(LogLevel.Debug, new EventId(12, "StreamAbort"), @"Stream id ""{ConnectionId}"" aborted by application because: ""{Reason}"".", skipEnabledCheck: true); private readonly ILogger _logger; @@ -52,7 +60,15 @@ public void AcceptedStream(QuicStreamContext streamContext) { if (_logger.IsEnabled(LogLevel.Debug)) { - _acceptedStream(_logger, streamContext.ConnectionId, null); + _acceptedStream(_logger, streamContext.ConnectionId, GetStreamType(streamContext), null); + } + } + + public void ConnectedStream(QuicStreamContext streamContext) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _connectedStream(_logger, streamContext.ConnectionId, GetStreamType(streamContext), null); } } @@ -64,6 +80,22 @@ public void ConnectionError(BaseConnectionContext connection, Exception ex) } } + public void ConnectionAborted(BaseConnectionContext connection, Exception ex) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _connectionAborted(_logger, connection.ConnectionId, ex); + } + } + + public void ConnectionAbort(BaseConnectionContext connection, string reason) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _connectionAbort(_logger, connection.ConnectionId, reason, null); + } + } + public void StreamError(QuicStreamContext streamContext, Exception ex) { if (_logger.IsEnabled(LogLevel.Debug)) @@ -96,12 +128,31 @@ public void StreamShutdownWrite(QuicStreamContext streamContext, string reason) } } + public void StreamAborted(QuicStreamContext streamContext, Exception ex) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _streamAborted(_logger, streamContext.ConnectionId, ex); + } + } + public void StreamAbort(QuicStreamContext streamContext, string reason) { if (_logger.IsEnabled(LogLevel.Debug)) { - _streamAborted(_logger, streamContext.ConnectionId, reason, null); + _streamAbort(_logger, streamContext.ConnectionId, reason, null); } } + + private StreamType GetStreamType(QuicStreamContext streamContext) => + streamContext.CanRead && streamContext.CanWrite + ? StreamType.Bidirectional + : StreamType.Unidirectional; + + private enum StreamType + { + Unidirectional, + Bidirectional + } } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs index 050a7ee8c5d3..71e850386747 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic { + // Not used anywhere. Remove? internal class QuicConnectionFactory : IMultiplexedConnectionFactory { private readonly QuicTransportContext _transportContext; diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index f51f2bc6580b..0dda1ab7a76e 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -34,7 +34,7 @@ public QuicTransportFactory(ILoggerFactory loggerFactory, IOptions(serverStream); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + Assert.True(quicStreamContext.CanWrite); + Assert.True(quicStreamContext.CanRead); + + Assert.Contains(LogMessages, m => m.Message.Contains("send loop completed gracefully")); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task BidirectionalStream_ClientAbortWrite_ServerReceivesAbort() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout(); + + // Act + await using var clientStream = quicConnection.OpenBidirectionalStream(); + await clientStream.WriteAsync(TestData).DefaultTimeout(); + + await using var serverStream = await serverConnection.AcceptAsync().DefaultTimeout(); + var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout(); + serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End); + + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + serverStream.ConnectionClosed.Register(() => closedTcs.SetResult()); + + clientStream.AbortWrite((long)Http3ErrorCode.InternalError); + + // Receive abort from client. + var ex = await Assert.ThrowsAsync(() => serverStream.Transport.Input.ReadAsync().AsTask()).DefaultTimeout(); + + // Server completes its output. + await serverStream.Transport.Output.CompleteAsync(); + + // Assert + Assert.Equal((long)Http3ErrorCode.InternalError, ((QuicStreamAbortedException)ex.InnerException).ErrorCode); + + var quicStreamContext = Assert.IsType(serverStream); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + + await closedTcs.Task.DefaultTimeout(); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task ClientToServerUnidirectionalStream_ServerReadsData_GracefullyClosed() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout(); + + // Act + await using var clientStream = quicConnection.OpenUnidirectionalStream(); + await clientStream.WriteAsync(TestData, endStream: true).DefaultTimeout(); + + await using var serverStream = await serverConnection.AcceptAsync().DefaultTimeout(); + var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout(); + serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End); + + // Input should be completed. + readResult = await serverStream.Transport.Input.ReadAsync(); + + // Assert + Assert.True(readResult.IsCompleted); + + var quicStreamContext = Assert.IsType(serverStream); + Assert.False(quicStreamContext.CanWrite); + Assert.True(quicStreamContext.CanRead); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task ClientToServerUnidirectionalStream_ClientAbort_ServerReceivesAbort() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout(); + + // Act + await using var clientStream = quicConnection.OpenUnidirectionalStream(); + await clientStream.WriteAsync(TestData).DefaultTimeout(); + + await using var serverStream = await serverConnection.AcceptAsync().DefaultTimeout(); + var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout(); + serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End); + + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + serverStream.ConnectionClosed.Register(() => closedTcs.SetResult()); + + clientStream.AbortWrite((long)Http3ErrorCode.InternalError); + + // Receive abort from client. + var ex = await Assert.ThrowsAsync(() => serverStream.Transport.Input.ReadAsync().AsTask()).DefaultTimeout(); + + // Assert + Assert.Equal((long)Http3ErrorCode.InternalError, ((QuicStreamAbortedException)ex.InnerException).ErrorCode); + + var quicStreamContext = Assert.IsType(serverStream); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + + await closedTcs.Task.DefaultTimeout(); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task ServerToClientUnidirectionalStream_ServerWritesDataAndCompletes_GracefullyClosed() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout(); + + // Act + var features = new FeatureCollection(); + features.Set(new DefaultStreamDirectionFeature(canRead: false, canWrite: true)); + var serverStream = await serverConnection.ConnectAsync(features).DefaultTimeout(); + await serverStream.Transport.Output.WriteAsync(TestData).DefaultTimeout(); + + await using var clientStream = await quicConnection.AcceptStreamAsync(); + + var data = new List(); + var buffer = new byte[1024]; + var readCount = 0; + while ((readCount = await clientStream.ReadAsync(buffer).DefaultTimeout()) != -1) + { + data.AddRange(buffer.AsMemory(0, readCount).ToArray()); + if (data.Count == TestData.Length) + { + break; + } + } + Assert.Equal(TestData, data); + + await serverStream.Transport.Output.CompleteAsync(); + + readCount = await clientStream.ReadAsync(buffer).DefaultTimeout(); + + // Assert + Assert.Equal(0, readCount); + + var quicStreamContext = Assert.IsType(serverStream); + Assert.True(quicStreamContext.CanWrite); + Assert.False(quicStreamContext.CanRead); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task ServerToClientUnidirectionalStream_ServerAborts_ClientGetsAbort() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout(); + + // Act + var features = new FeatureCollection(); + features.Set(new DefaultStreamDirectionFeature(canRead: false, canWrite: true)); + var serverStream = await serverConnection.ConnectAsync(features).DefaultTimeout(); + await serverStream.Transport.Output.WriteAsync(TestData).DefaultTimeout(); + + await using var clientStream = await quicConnection.AcceptStreamAsync(); + + var data = new List(); + var buffer = new byte[1024]; + var readCount = 0; + while ((readCount = await clientStream.ReadAsync(buffer).DefaultTimeout()) != -1) + { + data.AddRange(buffer.AsMemory(0, readCount).ToArray()); + if (data.Count == TestData.Length) + { + break; + } + } + Assert.Equal(TestData, data); + + ((IProtocolErrorCodeFeature)serverStream).Error = (long)Http3ErrorCode.InternalError; + serverStream.Abort(new ConnectionAbortedException("Test message")); + + // TODO - client isn't getting abort? + readCount = await clientStream.ReadAsync(buffer).DefaultTimeout(); + + // Assert + Assert.Equal(0, readCount); + + var quicStreamContext = Assert.IsType(serverStream); + Assert.True(quicStreamContext.CanWrite); + Assert.False(quicStreamContext.CanRead); + + // Both send and receive loops have exited. + await quicStreamContext._processingTask.DefaultTimeout(); + } + } +} diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs index e478d55a7935..d36b69ae19d0 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -20,18 +21,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests { internal static class QuicTestHelpers { - public static QuicTransportFactory CreateTransportFactory() + private const string Alpn = "h3-29"; + + public static QuicTransportFactory CreateTransportFactory(ILoggerFactory loggerFactory = null) { var quicTransportOptions = new QuicTransportOptions(); - quicTransportOptions.Alpn = "h3-29"; + quicTransportOptions.Alpn = Alpn; quicTransportOptions.IdleTimeout = TimeSpan.FromMinutes(1); - return new QuicTransportFactory(NullLoggerFactory.Instance, Options.Create(quicTransportOptions)); + return new QuicTransportFactory(loggerFactory ?? NullLoggerFactory.Instance, Options.Create(quicTransportOptions)); } - public static async Task CreateConnectionListenerFactory() + public static async Task CreateConnectionListenerFactory(ILoggerFactory loggerFactory = null) { - var transportFactory = CreateTransportFactory(); + var transportFactory = CreateTransportFactory(loggerFactory); // Use ephemeral port 0. OS will assign unused port. var endpoint = new IPEndPoint(IPAddress.Loopback, 0); @@ -70,7 +73,7 @@ public static QuicClientConnectionOptions CreateClientConnectionOptions(EndPoint { ApplicationProtocols = new List { - new SslApplicationProtocol("h3-29") + new SslApplicationProtocol(Alpn) }, RemoteCertificateValidationCallback = RemoteCertificateValidationCallback } diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs index 66a48dfde027..4e058f78153f 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests { - public class QuicTransportFactoryTests + public class QuicTransportFactoryTests : TestApplicationErrorLoggerLoggedTest { [ConditionalFact] [MsQuicSupported] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 62d0beecc511..c2dac4f8c845 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -33,8 +33,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class HttpsConnectionMiddlewareTests : LoggedTest { - private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); - private static X509Certificate2 _x509Certificate2NoExt = TestResources.GetTestCertificate("no_extensions.pfx"); + private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); + private static readonly X509Certificate2 _x509Certificate2NoExt = TestResources.GetTestCertificate("no_extensions.pfx"); [Fact] public async Task CanReadAndWriteWithHttpsConnectionMiddleware() From b9d7c3298b7e47f4b8585504a684c1e7d1b87478 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 2 Jul 2021 15:43:48 -0700 Subject: [PATCH 06/15] Unquarantine fixed test (#34044) --- .../test/Sockets.BindTests/SocketTransportOptionsTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs index 9c71482d54d7..c8ccbf00bb13 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -52,7 +52,6 @@ public Task SocketTransportCallsCreateBoundListenSocketForNewEndpoints(EndPoint } [Fact] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/33206")] public async Task SocketTransportCallsCreateBoundListenSocketForFileHandleEndpoint() { using var fileHandleSocket = CreateBoundSocket(); @@ -70,7 +69,6 @@ public void CreateDefaultBoundListenSocket_BindsForNewEndPoints(EndPoint endpoin } [Fact] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/33206")] public void CreateDefaultBoundListenSocket_PreservesLocalEndpointFromFileHandleEndpoint() { using var fileHandleSocket = CreateBoundSocket(); From 83cc8be71b9e38490898a19b273983d02f569a8e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 3 Jul 2021 18:03:26 +1200 Subject: [PATCH 07/15] HTTP/3: Complete outbound control stream with connection (#33956) --- .../src/Internal/Http3/Http3Connection.cs | 58 ++++++++++++------- .../Http3/Http3ConnectionErrorException.cs | 2 +- .../Http3/Http3ConnectionTests.cs | 10 +++- .../Http3/Http3TestBase.cs | 13 ++++- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 52759a2981ab..ac282e81011d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -224,14 +224,21 @@ public async Task ProcessRequestsAsync(IHttpApplication appl // the maximum capacity of the dynamic table to zero. // Don't create Encoder and Decoder as they aren't used now. - Exception? error = null; - // Don't delay setting up the connection on the control stream being ready. - // Task is awaited when connection finishes. - var controlStreamTask = CreateControlStreamAsync(application); + Exception? error = null; + ValueTask outboundControlStreamTask = default; try { + var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application); + lock (_sync) + { + OutboundControlStream = outboundControlStream; + } + + // Don't delay on waiting to send outbound control stream settings. + outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream); + while (_isClosed == 0) { // Don't pass a cancellation token to AcceptAsync. @@ -341,23 +348,26 @@ public async Task ProcessRequestsAsync(IHttpApplication appl stream.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error); } + lock (_sync) + { + OutboundControlStream?.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error); + } + while (_activeRequestCount > 0) { await _streamCompletionAwaitable; } + await outboundControlStreamTask; + _context.TimeoutControl.CancelTimeout(); _context.TimeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize); } catch { - Abort(connectionError, Http3ErrorCode.NoError); + Abort(connectionError, Http3ErrorCode.InternalError); throw; } - - // Ensure control stream creation task finished. At this point the connection, including the control - // stream should be closed/aborted. Error handling inside method ensures await won't throw. - await controlStreamTask; } } @@ -400,18 +410,12 @@ private void UpdateConnectionState() } } - private async ValueTask CreateControlStreamAsync(IHttpApplication application) where TContext : notnull + private async ValueTask ProcessOutboundControlStreamAsync(Http3ControlStream controlStream) { try { - var stream = await CreateNewUnidirectionalStreamAsync(application); - lock (_sync) - { - OutboundControlStream = stream; - } - - await stream.SendStreamIdAsync(id: 0); - await stream.SendSettingsFrameAsync(); + await controlStream.SendStreamIdAsync(id: 0); + await controlStream.SendSettingsFrameAsync(); } catch (Exception ex) { @@ -449,15 +453,27 @@ private async ValueTask CreateNewUnidirectionalStreamAsync(application, httpConnectionContext); } - private ValueTask SendGoAway(long id) + private async ValueTask SendGoAway(long id) { + Http3ControlStream? stream; lock (_sync) { - if (OutboundControlStream != null) + stream = OutboundControlStream; + } + + if (stream != null) + { + try + { + return await stream.SendGoAway(id); + } + catch { - return OutboundControlStream.SendGoAway(id); + // The control stream may not be healthy. + // Ignore error sending go away. } } + return default; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs index c7a63a926d12..746264ffbb89 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 internal class Http3ConnectionErrorException : Exception { public Http3ConnectionErrorException(string message, Http3ErrorCode errorCode) - : base($"HTTP/3 connection error ({errorCode}): {message}") + : base($"HTTP/3 connection error ({Http3Formatting.ToFormattedErrorCode(errorCode)}): {message}") { ErrorCode = errorCode; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 82ef555a7017..ae4223dc2f3c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -160,7 +160,15 @@ await WaitForConnectionErrorAsync( [Fact] public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError() { - OnCreateServerControlStream = () => throw new Exception(); + OnCreateServerControlStream = () => + { + var controlStream = new Http3ControlStream(this, StreamInitiator.Server); + + // Make server connection error when trying to write to control stream. + controlStream.StreamContext.Transport.Output.Complete(); + + return controlStream; + }; await InitializeConnectionAsync(_noopApplication); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index ba9eab872d1f..cf388abededa 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -187,13 +187,16 @@ internal async ValueTask GetInboundControlStream() if (_inboundControlStream == null) { var reader = MultiplexedConnectionContext.ToClientAcceptQueue.Reader; - while (await reader.WaitToReadAsync()) + while (await reader.WaitToReadAsync().DefaultTimeout()) { while (reader.TryRead(out var stream)) { _inboundControlStream = stream; var streamId = await stream.TryReadStreamIdAsync(); - Debug.Assert(streamId == 0, "StreamId sent that was non-zero, which isn't handled by tests"); + + // -1 means stream was completed. + Debug.Assert(streamId == 0 || streamId == -1, "StreamId sent that was non-zero, which isn't handled by tests"); + return _inboundControlStream; } } @@ -231,6 +234,12 @@ internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAway } AssertConnectionError(expectedErrorCode, expectedErrorMessage); + + // Verify HttpConnection.ProcessRequestsAsync has exited. + await _connectionTask.DefaultTimeout(); + + // Verify server-to-client control stream has completed. + await _inboundControlStream.ReceiveEndAsync(); } internal void AssertConnectionError(Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) where TException : Exception From 932c553b5edadc3d4fa233d04ddd1eb839a64772 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 2 Jul 2021 23:21:51 -0700 Subject: [PATCH 08/15] Update selenium versions (#34045) * Update selenium versions * React to selenium updates --- eng/Versions.props | 4 ++-- .../benchmarkapps/Wasm.Performance/Driver/Selenium.cs | 2 +- src/Components/test/E2ETest/Tests/FormsTest.cs | 4 ++-- src/Shared/E2ETesting/BrowserFixture.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index b2796069544d..4f6e1bea79e1 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -250,9 +250,9 @@ 0.192.0 3.0.0 7.2.2 - 4.0.0-beta2 + 4.0.0-beta4 91.0.4472.1900 - 4.0.0-beta2 + 4.0.0-beta4 1.4.0 4.0.0 2.2.4 diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs index b8ba5b1e26a1..666e1a0ea7fc 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs @@ -93,7 +93,7 @@ public static async Task CreateBrowser(CancellationToken cancel if (PoolForBrowserLogs) { // Run in background. - var logs = new RemoteLogs(driver); + var logs = driver.Manage().Logs; _ = Task.Run(async () => { while (!cancellationToken.IsCancellationRequested) diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index aef12b8f2ea2..5440602e4141 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -608,8 +608,8 @@ public void InputRangeAttributeOrderDoesNotAffectValue() var rangeWithValueLast = appElement.FindElement(By.Id("range-value-last")); // Value never gets incorrectly clamped. - Browser.Equal("210", () => rangeWithValueFirst.GetProperty("value")); - Browser.Equal("210", () => rangeWithValueLast.GetProperty("value")); + Browser.Equal("210", () => rangeWithValueFirst.GetDomProperty("value")); + Browser.Equal("210", () => rangeWithValueLast.GetDomProperty("value")); } private Func CreateValidationMessagesAccessor(IWebElement appElement) diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs index f7d5c8d62a44..a8ca2c0507c9 100644 --- a/src/Shared/E2ETesting/BrowserFixture.cs +++ b/src/Shared/E2ETesting/BrowserFixture.cs @@ -198,7 +198,7 @@ private async Task DeleteBrowserUserProfileDirectoriesAsync() TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60))); driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1); - var logs = new RemoteLogs(driver); + var logs = driver.Manage().Logs; return (driver, logs); } @@ -332,7 +332,7 @@ private string UserProfileDirectory(string context) // Make sure implicit waits are disabled as they don't mix well with explicit waiting // see https://www.selenium.dev/documentation/en/webdriver/waits/#implicit-wait driver.Manage().Timeouts().ImplicitWait = TimeSpan.Zero; - var logs = new RemoteLogs(driver); + var logs = driver.Manage().Logs; return (driver, logs); } From 49042d17801a6be09201b334ea8becd0e74d03ff Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 3 Jul 2021 13:37:15 +0100 Subject: [PATCH 09/15] Set DisplayName on ActionDescriptor (#34063) Copy the DisplayName from the route endpoint to the ActionDescriptor. Addresses #34062. --- .../EndpointMetadataApiDescriptionProvider.cs | 1 + ...ndpointMetadataApiDescriptionProviderTest.cs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index b646cdcca1b0..0ab8f7e113f6 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -91,6 +91,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string RelativePath = routeEndpoint.RoutePattern.RawText?.TrimStart('/'), ActionDescriptor = new ActionDescriptor { + DisplayName = routeEndpoint.DisplayName, RouteValues = { ["controller"] = controllerName, diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 093543d8b93f..231c2f3b20cd 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -302,10 +302,19 @@ public void AddsMultipleParameters() Assert.Equal(BindingSource.Body, fromBodyParam.Source); } + [Fact] + public void AddsDisplayNameFromRouteEndpoint() + { + var apiDescription = GetApiDescription(() => "foo", displayName: "FOO"); + + Assert.Equal("FOO", apiDescription.ActionDescriptor.DisplayName); + } + private IList GetApiDescriptions( Delegate action, string pattern = null, - IEnumerable httpMethods = null) + IEnumerable httpMethods = null, + string displayName = null) { var methodInfo = action.Method; var attributes = methodInfo.GetCustomAttributes(); @@ -316,7 +325,7 @@ private IList GetApiDescriptions( var endpointMetadata = new EndpointMetadataCollection(metadataItems.ToArray()); var routePattern = RoutePatternFactory.Parse(pattern ?? "/"); - var endpoint = new RouteEndpoint(httpContext => Task.CompletedTask, routePattern, 0, endpointMetadata, null); + var endpoint = new RouteEndpoint(httpContext => Task.CompletedTask, routePattern, 0, endpointMetadata, displayName); var endpointDataSource = new DefaultEndpointDataSource(endpoint); var hostEnvironment = new HostEnvironment { @@ -331,8 +340,8 @@ private IList GetApiDescriptions( return context.Results; } - private ApiDescription GetApiDescription(Delegate action, string pattern = null) => - Assert.Single(GetApiDescriptions(action, pattern)); + private ApiDescription GetApiDescription(Delegate action, string pattern = null, string displayName = null) => + Assert.Single(GetApiDescriptions(action, pattern, displayName: displayName)); private static void TestAction() { From 000a8039d98cbd7cf274b3e093b75052dd017e09 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 3 Jul 2021 13:38:33 +0100 Subject: [PATCH 10/15] Apply code suggestions from Visual Studio (#34064) Applies a number of Visual Studio suggestions and errors found while working on #34063. --- src/Http/Routing/src/ModelEndpointDataSource.cs | 2 +- .../Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs | 6 +++--- .../src/EndpointMetadataApiDescriptionProvider.cs | 2 +- src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/Routing/src/ModelEndpointDataSource.cs b/src/Http/Routing/src/ModelEndpointDataSource.cs index c9212c457b4d..5ac84828a259 100644 --- a/src/Http/Routing/src/ModelEndpointDataSource.cs +++ b/src/Http/Routing/src/ModelEndpointDataSource.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing { internal class ModelEndpointDataSource : EndpointDataSource { - private List _endpointConventionBuilders; + private readonly List _endpointConventionBuilders; public ModelEndpointDataSource() { diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index ea388cdfbb36..a2f5a827523a 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -338,7 +338,7 @@ private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter) }; } - private IEnumerable GetHttpMethods(ControllerActionDescriptor action) + private static IEnumerable GetHttpMethods(ControllerActionDescriptor action) { if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) { @@ -350,7 +350,7 @@ private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter) } } - private RouteTemplate? ParseTemplate(ControllerActionDescriptor action) + private static RouteTemplate? ParseTemplate(ControllerActionDescriptor action) { if (action.AttributeRouteInfo?.Template != null) { @@ -445,7 +445,7 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList - new EndpointModelMetadata(ModelMetadataIdentity.ForType(type)); + new(ModelMetadataIdentity.ForType(type)); private static void AddResponseContentTypes(IList apiResponseFormats, IReadOnlyList contentTypes) { diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs index 03e96036692b..56b7a45614a7 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs @@ -304,7 +304,7 @@ private static (RoutePattern resolvedRoutePattern, IDictionary return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues); } - private void AddActionDataToBuilder( + private static void AddActionDataToBuilder( EndpointBuilder builder, HashSet routeNames, ActionDescriptor action, From 1b819d05de41561cc974af066737c38ded877ef7 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 3 Jul 2021 15:17:10 +0100 Subject: [PATCH 11/15] Support Request, Response and User for minimal actions (#33883) Adds support to minimal actions for parameters of type HttpRequest, HttpResponse, and ClaimsPrincipal to be bound to the values of the Request, Response and User properties of the HttpContext respectively. Also cleans up some typos in the RequestDelegateFactory tests. Addresses #33870. --- .../src/RequestDelegateFactory.cs | 14 ++ .../test/RequestDelegateFactoryTests.cs | 147 +++++++++++++----- 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index d65af58ff3ef..5f3ab3af5769 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Metadata; @@ -45,6 +46,7 @@ public static class RequestDelegateFactory private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Request)); private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Response)); private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestAborted)); + private static readonly MemberExpression UserExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.User)); private static readonly MemberExpression RouteValuesExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues)); private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query)); private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers)); @@ -221,6 +223,18 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext { return HttpContextExpr; } + else if (parameter.ParameterType == typeof(HttpRequest)) + { + return HttpRequestExpr; + } + else if (parameter.ParameterType == typeof(HttpResponse)) + { + return HttpResponseExpr; + } + else if (parameter.ParameterType == typeof(ClaimsPrincipal)) + { + return UserExpr; + } else if (parameter.ParameterType == typeof(CancellationToken)) { return RequestAbortedExpr; diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 9d70808e4828..5d2f342aee89 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -14,6 +14,7 @@ using System.Numerics; using System.Reflection; using System.Reflection.Metadata; +using System.Security.Claims; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -97,7 +98,7 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate) { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -117,7 +118,7 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); - var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvider()); var httpContext = new DefaultHttpContext(); @@ -163,7 +164,7 @@ object GetTarget() return new TestNonStaticActionClass(2); } - var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvdier(), _ => GetTarget()); + var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvider(), _ => GetTarget()); var httpContext = new DefaultHttpContext(); @@ -186,7 +187,7 @@ public void BuildRequestDelegateThrowsArgumentNullExceptions() BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); - var serviceProvider = new EmptyServiceProvdier(); + var serviceProvider = new EmptyServiceProvider(); var exNullAction = Assert.Throws(() => RequestDelegateFactory.Create(action: null!, serviceProvider)); var exNullMethodInfo1 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!, serviceProvider)); @@ -205,7 +206,7 @@ public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName const string paramName = "value"; const int originalRouteParam = 42; - void TestAction(HttpContext httpContext, [FromRoute] int value) + static void TestAction(HttpContext httpContext, [FromRoute] int value) { httpContext.Items.Add("input", value); } @@ -213,7 +214,7 @@ void TestAction(HttpContext httpContext, [FromRoute] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -240,7 +241,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -252,7 +253,7 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -264,7 +265,7 @@ public async Task RequestDelegatePopulatesFromOptionalStringParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptionalString, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestOptionalString, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -281,7 +282,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -304,7 +305,7 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -327,7 +328,7 @@ void TestAction([FromRoute] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -406,7 +407,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues["tryParsable"] = routeValue; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -423,7 +424,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ ["tryParsable"] = routeValue }); - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -446,7 +447,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR { httpContext.Items["tryParsable"] = tryParsable; }), - new EmptyServiceProvdier()); + new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -474,7 +475,7 @@ void InvalidFromHeader([FromHeader] object notTryParsable) { } [MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))] public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) { - var ex = Assert.Throws(() => RequestDelegateFactory.Create(action, new EmptyServiceProvdier())); + var ex = Assert.Throws(() => RequestDelegateFactory.Create(action, new EmptyServiceProvider())); Assert.Equal("No public static bool Object.TryParse(string, out Object) method found for notTryParsable.", ex.Message); } @@ -483,7 +484,7 @@ public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() { var unnamedParameter = Expression.Parameter(typeof(int)); var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); - var ex = Assert.Throws(() => RequestDelegateFactory.Create((Action)lambda.Compile(), new EmptyServiceProvdier())); + var ex = Assert.Throws(() => RequestDelegateFactory.Create((Action)lambda.Compile(), new EmptyServiceProvider())); Assert.Equal("A parameter does not have a name! Was it generated? All parameters must be named.", ex.Message); } @@ -506,7 +507,7 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -548,7 +549,7 @@ void TestAction([FromQuery] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Query = query; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -571,7 +572,7 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -635,7 +636,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) }); httpContext.RequestServices = mock.Object; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -652,7 +653,7 @@ public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -671,7 +672,7 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -695,7 +696,7 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -722,7 +723,7 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -755,7 +756,7 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -776,9 +777,9 @@ void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { void TestInferredInvalidAction(Todo value1, Todo value2) { } void TestBothInvalidAction(Todo value1, [FromBody] int value2) { } - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestAttributedInvalidAction, new EmptyServiceProvdier())); - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestInferredInvalidAction, new EmptyServiceProvdier())); - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestBothInvalidAction, new EmptyServiceProvdier())); + Assert.Throws(() => RequestDelegateFactory.Create((Action)TestAttributedInvalidAction, new EmptyServiceProvider())); + Assert.Throws(() => RequestDelegateFactory.Create((Action)TestInferredInvalidAction, new EmptyServiceProvider())); + Assert.Throws(() => RequestDelegateFactory.Create((Action)TestBothInvalidAction, new EmptyServiceProvider())); } public static object[][] FromServiceActions @@ -850,9 +851,9 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Delegate action) { var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new EmptyServiceProvdier(); + httpContext.RequestServices = new EmptyServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -869,7 +870,7 @@ void TestAction(HttpContext httpContext) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -877,7 +878,7 @@ void TestAction(HttpContext httpContext) } [Fact] - public async Task RequestDelegatePassHttpContextRequestAbortedAsCancelationToken() + public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() { CancellationToken? cancellationTokenArgument = null; @@ -892,13 +893,73 @@ void TestAction(CancellationToken cancellationToken) RequestAborted = cts.Token }; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); await requestDelegate(httpContext); Assert.Equal(httpContext.RequestAborted, cancellationTokenArgument); } + [Fact] + public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() + { + ClaimsPrincipal? userArgument = null; + + void TestAction(ClaimsPrincipal user) + { + userArgument = user; + } + + var httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal() + }; + + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + + await requestDelegate(httpContext); + + Assert.Equal(httpContext.User, userArgument); + } + + [Fact] + public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() + { + HttpRequest? httpRequestArgument = null; + + void TestAction(HttpRequest httpRequest) + { + httpRequestArgument = httpRequest; + } + + var httpContext = new DefaultHttpContext(); + + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + + await requestDelegate(httpContext); + + Assert.Equal(httpContext.Request, httpRequestArgument); + } + + [Fact] + public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse() + { + HttpResponse? httpResponseArgument = null; + + void TestAction(HttpResponse httpResponse) + { + httpResponseArgument = httpResponse; + } + + var httpContext = new DefaultHttpContext(); + + var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + + await requestDelegate(httpContext); + + Assert.Equal(httpContext.Response, httpResponseArgument); + } + public static IEnumerable ComplexResult { get @@ -936,7 +997,7 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -985,7 +1046,7 @@ public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -1028,7 +1089,7 @@ public async Task RequestDelegateWritesStringReturnValueAsJsonResponseBody(Deleg var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -1069,7 +1130,7 @@ public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -1110,7 +1171,7 @@ public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvdier()); + var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); await requestDelegate(httpContext); @@ -1228,7 +1289,7 @@ class TodoJsonConverter : JsonConverter break; } - string property = reader.GetString()!; + var property = reader.GetString()!; reader.Read(); switch (property.ToLowerInvariant()) @@ -1353,13 +1414,13 @@ public override void Write(byte[] buffer, int offset, int count) } } - private class EmptyServiceProvdier : IServiceScope, IServiceProvider, IServiceScopeFactory + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory { public IServiceProvider ServiceProvider => this; public IServiceScope CreateScope() { - return new EmptyServiceProvdier(); + return new EmptyServiceProvider(); } public void Dispose() @@ -1379,7 +1440,7 @@ public void Dispose() private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature { - private readonly CancellationTokenSource _requestAbortedCts = new CancellationTokenSource(); + private readonly CancellationTokenSource _requestAbortedCts = new(); public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); } From 5936bd41c32628345bb687e5d457170641b1cdb9 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 5 Jul 2021 18:04:44 +0100 Subject: [PATCH 12/15] Component parameters from querystring (#34038) --- .../test/AuthorizeRouteViewTest.cs | 7 +- .../Microsoft.AspNetCore.Components.csproj | 2 + .../Components/src/PublicAPI.Unshipped.txt | 4 + .../src/Reflection/ComponentProperties.cs | 10 +- .../Components/src/RenderTree/Renderer.cs | 1 + src/Components/Components/src/RouteView.cs | 16 + .../Routing/QueryParameterValueSupplier.cs | 205 +++++++ .../Components/src/Routing/RouteConstraint.cs | 85 +-- .../src/Routing/StringSegmentAccumulator.cs | 67 +++ .../Components/src/Routing/TemplateSegment.cs | 10 +- .../src/Routing/TypeRouteConstraint.cs | 51 -- .../src/Routing/UrlValueConstraint.cs | 176 ++++++ .../src/SupplyParameterFromQueryAttribute.cs | 21 + .../QueryParameterValueSupplierTest.cs | 513 ++++++++++++++++++ .../test/E2ETest/Tests/RoutingTest.cs | 89 ++- .../BasicTestApp/RouterTest/Links.razor | 2 + .../RouterTest/WithQueryParameters.razor | 32 ++ 17 files changed, 1160 insertions(+), 131 deletions(-) create mode 100644 src/Components/Components/src/Routing/QueryParameterValueSupplier.cs create mode 100644 src/Components/Components/src/Routing/StringSegmentAccumulator.cs delete mode 100644 src/Components/Components/src/Routing/TypeRouteConstraint.cs create mode 100644 src/Components/Components/src/Routing/UrlValueConstraint.cs create mode 100644 src/Components/Components/src/SupplyParameterFromQueryAttribute.cs create mode 100644 src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index 9951014a5193..58c29459e8ec 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Authorization { public class AuthorizeRouteViewTest { - private readonly static IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); + private static readonly IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); private readonly TestAuthenticationStateProvider _authenticationStateProvider; private readonly TestRenderer _renderer; private readonly RouteView _authorizeRouteViewComponent; @@ -35,6 +35,7 @@ public AuthorizeRouteViewTest() serviceCollection.AddSingleton(_authenticationStateProvider); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(_testAuthorizationService); + serviceCollection.AddSingleton(); _renderer = new TestRenderer(serviceCollection.BuildServiceProvider()); _authorizeRouteViewComponent = new AuthorizeRouteView(); @@ -424,5 +425,9 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.CloseComponent(); } } + + class TestNavigationManager : NavigationManager + { + } } } diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 9a75de9cca0d..7518710f7f6b 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -7,6 +7,7 @@ true enable true + true @@ -14,6 +15,7 @@ + diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 8e6ae930e781..bb22b4def547 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -57,6 +57,10 @@ Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string! Microsoft.AspNetCore.Components.RenderTree.Renderer.GetEventArgsType(ulong eventHandlerId) -> System.Type! +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.SupplyParameterFromQueryAttribute() -> void abstract Microsoft.AspNetCore.Components.ErrorBoundaryBase.OnErrorAsync(System.Exception! exception) -> System.Threading.Tasks.Task! override Microsoft.AspNetCore.Components.LayoutComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary! parameters) -> Microsoft.AspNetCore.Components.ParameterView diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 0f45353abc63..90e09b56af5d 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -12,12 +12,12 @@ namespace Microsoft.AspNetCore.Components.Reflection { internal static class ComponentProperties { - private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase; + internal const BindingFlags BindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase; // Right now it's not possible for a component to define a Parameter and a Cascading Parameter with // the same name. We don't give you a way to express this in code (would create duplicate properties), // and we don't have the ability to represent it in our data structures. - private readonly static ConcurrentDictionary _cachedWritersByType + private static readonly ConcurrentDictionary _cachedWritersByType = new ConcurrentDictionary(); public static void ClearCache() => _cachedWritersByType.Clear(); @@ -162,7 +162,7 @@ static void SetProperty(object target, PropertySetter writer, string parameterNa } internal static IEnumerable GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType) - => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags); + => MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags); [DoesNotReturn] private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMembers(Component)] Type targetType, @@ -170,7 +170,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem { // We know we're going to throw by this stage, so it doesn't matter that the following // reflection code will be slow. We're just trying to help developers see what they did wrong. - var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags); + var propertyInfo = targetType.GetProperty(parameterName, BindablePropertyFlags); if (propertyInfo != null) { if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute))) @@ -223,7 +223,7 @@ private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, stri private static void ThrowForMultipleCaptureUnmatchedValuesParameters([DynamicallyAccessedMembers(Component)] Type targetType) { var propertyNames = new List(); - foreach (var property in targetType.GetProperties(_bindablePropertyFlags)) + foreach (var property in targetType.GetProperties(BindablePropertyFlags)) { if (property.GetCustomAttribute()?.CaptureUnmatchedValues == true) { diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 82a74c0c4e3f..9217df4d41dd 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -130,6 +130,7 @@ private async void RenderRootComponentsOnHotReload() // Before re-rendering the root component, also clear any well-known caches in the framework _componentFactory.ClearCache(); ComponentProperties.ClearCache(); + Routing.QueryParameterValueSupplier.ClearCache(); await Dispatcher.InvokeAsync(() => { diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs index 76f9f47d311e..69753ba394af 100644 --- a/src/Components/Components/src/RouteView.cs +++ b/src/Components/Components/src/RouteView.cs @@ -4,9 +4,11 @@ #nullable disable warnings using System; +using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components { @@ -20,6 +22,9 @@ public class RouteView : IComponent private readonly RenderFragment _renderPageWithParametersDelegate; private RenderHandle _renderHandle; + [Inject] + private NavigationManager NavigationManager { get; set; } + /// /// Gets or sets the route data. This determines the page that will be /// displayed and the parameter values that will be supplied to the page. @@ -90,6 +95,17 @@ private void RenderPageWithParameters(RenderTreeBuilder builder) builder.AddAttribute(1, kvp.Key, kvp.Value); } + var queryParameterSupplier = QueryParameterValueSupplier.ForType(RouteData.PageType); + if (queryParameterSupplier is not null) + { + // Since this component does accept some parameters from query, we must supply values for all of them, + // even if the querystring in the URI is empty. So don't skip the following logic. + var url = NavigationManager.Uri; + var queryStartPos = url.IndexOf('?'); + var query = queryStartPos < 0 ? default : url.AsMemory(queryStartPos); + queryParameterSupplier.RenderParametersFromQueryString(builder, query); + } + builder.CloseComponent(); } } diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs new file mode 100644 index 000000000000..421a0b6d02ce --- /dev/null +++ b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs @@ -0,0 +1,205 @@ +// 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 System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.AspNetCore.Components.Reflection; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Internal; +using static Microsoft.AspNetCore.Internal.LinkerFlags; + +namespace Microsoft.AspNetCore.Components.Routing +{ + internal sealed class QueryParameterValueSupplier + { + public static void ClearCache() => _cacheByType.Clear(); + + private static readonly Dictionary _cacheByType = new(); + + // These two arrays contain the same number of entries, and their corresponding positions refer to each other. + // Holding the info like this means we can use Array.BinarySearch with less custom implementation. + private readonly ReadOnlyMemory[] _queryParameterNames; + private readonly QueryParameterDestination[] _destinations; + + public static QueryParameterValueSupplier? ForType([DynamicallyAccessedMembers(Component)] Type componentType) + { + if (!_cacheByType.TryGetValue(componentType, out var instanceOrNull)) + { + // If the component doesn't have any query parameters, store a null value for it + // so we know the upstream code can't try to render query parameter frames for it. + var sortedMappings = GetSortedMappings(componentType); + instanceOrNull = sortedMappings == null ? null : new QueryParameterValueSupplier(sortedMappings); + _cacheByType.TryAdd(componentType, instanceOrNull); + } + + return instanceOrNull; + } + + private QueryParameterValueSupplier(QueryParameterMapping[] sortedMappings) + { + _queryParameterNames = new ReadOnlyMemory[sortedMappings.Length]; + _destinations = new QueryParameterDestination[sortedMappings.Length]; + for (var i = 0; i < sortedMappings.Length; i++) + { + ref var mapping = ref sortedMappings[i]; + _queryParameterNames[i] = mapping.QueryParameterName; + _destinations[i] = mapping.Destination; + } + } + + public void RenderParametersFromQueryString(RenderTreeBuilder builder, ReadOnlyMemory queryString) + { + // If there's no querystring contents, we can skip renting from the pool + if (queryString.IsEmpty) + { + for (var destinationIndex = 0; destinationIndex < _destinations.Length; destinationIndex++) + { + ref var destination = ref _destinations[destinationIndex]; + var blankValue = destination.IsArray ? destination.Parser.ParseMultiple(default, string.Empty) : null; + builder.AddAttribute(0, destination.ComponentParameterName, blankValue); + } + return; + } + + // Temporary workspace in which we accumulate the data while walking the querystring. + var valuesByMapping = ArrayPool.Shared.Rent(_destinations.Length); + + try + { + // Capture values by destination in a single pass through the querystring + var queryStringEnumerable = new QueryStringEnumerable(queryString); + foreach (var suppliedPair in queryStringEnumerable) + { + var decodedName = suppliedPair.DecodeName(); + var mappingIndex = Array.BinarySearch(_queryParameterNames, decodedName, QueryParameterNameComparer.Instance); + if (mappingIndex >= 0) + { + var decodedValue = suppliedPair.DecodeValue(); + + if (_destinations[mappingIndex].IsArray) + { + valuesByMapping[mappingIndex].Add(decodedValue); + } + else + { + valuesByMapping[mappingIndex].SetSingle(decodedValue); + } + } + } + + // Finally, emit the parameter attributes by parsing all the string segments and building arrays + for (var mappingIndex = 0; mappingIndex < _destinations.Length; mappingIndex++) + { + ref var destination = ref _destinations[mappingIndex]; + ref var values = ref valuesByMapping[mappingIndex]; + + var parsedValue = destination.IsArray + ? destination.Parser.ParseMultiple(values, destination.ComponentParameterName) + : values.Count == 0 + ? default + : destination.Parser.Parse(values[0].Span, destination.ComponentParameterName); + + builder.AddAttribute(0, destination.ComponentParameterName, parsedValue); + } + } + finally + { + ArrayPool.Shared.Return(valuesByMapping, true); + } + } + + private static QueryParameterMapping[]? GetSortedMappings([DynamicallyAccessedMembers(Component)] Type componentType) + { + var candidateProperties = MemberAssignment.GetPropertiesIncludingInherited(componentType, ComponentProperties.BindablePropertyFlags); + HashSet>? usedQueryParameterNames = null; + List? mappings = null; + + foreach (var propertyInfo in candidateProperties) + { + if (!propertyInfo.IsDefined(typeof(ParameterAttribute))) + { + continue; + } + + var fromQueryAttribute = propertyInfo.GetCustomAttribute(); + if (fromQueryAttribute is not null) + { + // Found a parameter that's assignable from querystring + var componentParameterName = propertyInfo.Name; + var queryParameterName = (string.IsNullOrEmpty(fromQueryAttribute.Name) + ? componentParameterName + : fromQueryAttribute.Name).AsMemory(); + + // If it's an array type, capture that info and prepare to parse the element type + Type effectiveType = propertyInfo.PropertyType; + var isArray = false; + if (effectiveType.IsArray) + { + isArray = true; + effectiveType = effectiveType.GetElementType()!; + } + + if (!UrlValueConstraint.TryGetByTargetType(effectiveType, out var parser)) + { + throw new NotSupportedException($"Querystring values cannot be parsed as type '{propertyInfo.PropertyType}'."); + } + + // Add the destination for this component parameter name + usedQueryParameterNames ??= new(QueryParameterNameComparer.Instance); + if (usedQueryParameterNames.Contains(queryParameterName)) + { + throw new InvalidOperationException($"The component '{componentType}' declares more than one mapping for the query parameter '{queryParameterName}'."); + } + usedQueryParameterNames.Add(queryParameterName); + + mappings ??= new(); + mappings.Add(new QueryParameterMapping + { + QueryParameterName = queryParameterName, + Destination = new QueryParameterDestination(componentParameterName, parser, isArray) + }); + } + } + + mappings?.Sort((a, b) => QueryParameterNameComparer.Instance.Compare(a.QueryParameterName, b.QueryParameterName)); + return mappings?.ToArray(); + } + + private readonly struct QueryParameterMapping + { + public ReadOnlyMemory QueryParameterName { get; init; } + public QueryParameterDestination Destination { get; init; } + } + + private readonly struct QueryParameterDestination + { + public readonly string ComponentParameterName; + public readonly UrlValueConstraint Parser; + public readonly bool IsArray; + + public QueryParameterDestination(string componentParameterName, UrlValueConstraint parser, bool isArray) + { + ComponentParameterName = componentParameterName; + Parser = parser; + IsArray = isArray; + } + } + + private class QueryParameterNameComparer : IComparer>, IEqualityComparer> + { + public static readonly QueryParameterNameComparer Instance = new(); + + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.CompareTo(y.Span, StringComparison.OrdinalIgnoreCase); + + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.Equals(y.Span, StringComparison.OrdinalIgnoreCase); + + public int GetHashCode([DisallowNull] ReadOnlyMemory obj) + => string.GetHashCode(obj.Span, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Components/Components/src/Routing/RouteConstraint.cs b/src/Components/Components/src/Routing/RouteConstraint.cs index babfaf64dd3e..51dd00d3f5c4 100644 --- a/src/Components/Components/src/Routing/RouteConstraint.cs +++ b/src/Components/Components/src/Routing/RouteConstraint.cs @@ -2,87 +2,38 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; -using System.Globalization; namespace Microsoft.AspNetCore.Components.Routing { - internal abstract class RouteConstraint + internal static class RouteConstraint { - // note: the things that prevent this cache from growing unbounded is that - // we're the only caller to this code path, and the fact that there are only - // 8 possible instances that we create. - // - // The values passed in here for parsing are always static text defined in route attributes. - private static readonly ConcurrentDictionary _cachedConstraints - = new ConcurrentDictionary(); - - public abstract bool Match(string pathSegment, out object? convertedValue); - - public static RouteConstraint Parse(string template, string segment, string constraint) + public static UrlValueConstraint Parse(string template, string segment, string constraint) { if (string.IsNullOrEmpty(constraint)) { throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint."); } - if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance)) - { - return cachedInstance; - } - else + var targetType = GetTargetType(constraint); + if (targetType is null || !UrlValueConstraint.TryGetByTargetType(targetType, out var result)) { - var newInstance = CreateRouteConstraint(constraint); - if (newInstance != null) - { - // We've done to the work to create the constraint now, but it's possible - // we're competing with another thread. GetOrAdd can ensure only a single - // instance is returned so that any extra ones can be GC'ed. - return _cachedConstraints.GetOrAdd(constraint, newInstance); - } - else - { - throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'."); - } + throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'."); } + + return result; } - /// - /// Creates a structured RouteConstraint object given a string that contains - /// the route constraint. A constraint is the place after the colon in a - /// parameter definition, for example `{age:int?}`. - /// - /// String representation of the constraint - /// Type-specific RouteConstraint object - private static RouteConstraint? CreateRouteConstraint(string constraint) + private static Type? GetTargetType(string constraint) => constraint switch { - switch (constraint) - { - case "bool": - return new TypeRouteConstraint(bool.TryParse); - case "datetime": - return new TypeRouteConstraint((string str, out DateTime result) - => DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); - case "decimal": - return new TypeRouteConstraint((string str, out decimal result) - => decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "double": - return new TypeRouteConstraint((string str, out double result) - => double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "float": - return new TypeRouteConstraint((string str, out float result) - => float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "guid": - return new TypeRouteConstraint(Guid.TryParse); - case "int": - return new TypeRouteConstraint((string str, out int result) - => int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)); - case "long": - return new TypeRouteConstraint((string str, out long result) - => long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)); - default: - return null; - } - } + "bool" => typeof(bool), + "datetime" => typeof(DateTime), + "decimal" => typeof(decimal), + "double" => typeof(double), + "float" => typeof(float), + "guid" => typeof(Guid), + "int" => typeof(int), + "long" => typeof(long), + _ => null, + }; } } diff --git a/src/Components/Components/src/Routing/StringSegmentAccumulator.cs b/src/Components/Components/src/Routing/StringSegmentAccumulator.cs new file mode 100644 index 000000000000..fdf8bbd17ab0 --- /dev/null +++ b/src/Components/Components/src/Routing/StringSegmentAccumulator.cs @@ -0,0 +1,67 @@ +// 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 System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components.Routing +{ + // This is very similar to Microsoft.Extensions.Primitives.StringValues, except it works in terms + // of ReadOnlyMemory rather than string, so the querystring handling logic doesn't need to + // allocate per-value when tracking things that will be parsed as value types. + internal struct StringSegmentAccumulator + { + private int count; + private ReadOnlyMemory _single; + private List>? _multiple; + + public ReadOnlyMemory this[int index] + { + get + { + if (index >= count) + { + throw new IndexOutOfRangeException(); + } + + return count == 1 ? _single : _multiple![index]; + } + } + + public int Count => count; + + public void SetSingle(ReadOnlyMemory value) + { + _single = value; + + if (count != 1) + { + if (count > 1) + { + _multiple = null; + } + + count = 1; + } + } + + public void Add(ReadOnlyMemory value) + { + switch (count++) + { + case 0: + _single = value; + break; + case 1: + _multiple = new(); + _multiple.Add(_single); + _multiple.Add(value); + _single = default; + break; + default: + _multiple!.Add(value); + break; + } + } + } +} diff --git a/src/Components/Components/src/Routing/TemplateSegment.cs b/src/Components/Components/src/Routing/TemplateSegment.cs index cbb03e04bcf0..de28e3e79508 100644 --- a/src/Components/Components/src/Routing/TemplateSegment.cs +++ b/src/Components/Components/src/Routing/TemplateSegment.cs @@ -51,7 +51,7 @@ public TemplateSegment(string template, string segment, bool isParameter) throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name."); } - Constraints = Array.Empty(); + Constraints = Array.Empty(); } else { @@ -68,7 +68,7 @@ public TemplateSegment(string template, string segment, bool isParameter) tokens[^1] = tokens[^1][0..^1]; } - Constraints = new RouteConstraint[tokens.Length - 1]; + Constraints = new UrlValueConstraint[tokens.Length - 1]; for (var i = 1; i < tokens.Length; i++) { Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]); @@ -77,7 +77,7 @@ public TemplateSegment(string template, string segment, bool isParameter) } else { - Constraints = Array.Empty(); + Constraints = Array.Empty(); } if (IsParameter) @@ -106,7 +106,7 @@ public TemplateSegment(string template, string segment, bool isParameter) public bool IsCatchAll { get; } - public RouteConstraint[] Constraints { get; } + public UrlValueConstraint[] Constraints { get; } public bool Match(string pathSegment, out object? matchedParameterValue) { @@ -116,7 +116,7 @@ public bool Match(string pathSegment, out object? matchedParameterValue) foreach (var constraint in Constraints) { - if (!constraint.Match(pathSegment, out matchedParameterValue)) + if (!constraint.TryParse(pathSegment, out matchedParameterValue)) { return false; } diff --git a/src/Components/Components/src/Routing/TypeRouteConstraint.cs b/src/Components/Components/src/Routing/TypeRouteConstraint.cs deleted file mode 100644 index 1e2f9d3c1795..000000000000 --- a/src/Components/Components/src/Routing/TypeRouteConstraint.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNetCore.Components.Routing -{ - /// - /// A route constraint that requires the value to be parseable as a specified type. - /// - /// The type to which the value must be parseable. - internal class TypeRouteConstraint : RouteConstraint - { - public delegate bool TryParseDelegate(string str, [MaybeNullWhen(false)] out T result); - - private readonly TryParseDelegate _parser; - - public TypeRouteConstraint(TryParseDelegate parser) - { - _parser = parser; - } - - public override bool Match(string pathSegment, out object? convertedValue) - { - if (_parser(pathSegment, out var result)) - { - convertedValue = result; - return true; - } - else - { - convertedValue = null; - return false; - } - } - - public override string ToString() => typeof(T) switch - { - var x when x == typeof(bool) => "bool", - var x when x == typeof(DateTime) => "datetime", - var x when x == typeof(decimal) => "decimal", - var x when x == typeof(double) => "double", - var x when x == typeof(float) => "float", - var x when x == typeof(Guid) => "guid", - var x when x == typeof(int) => "int", - var x when x == typeof(long) => "long", - var x => x.Name.ToLowerInvariant() - }; - } -} diff --git a/src/Components/Components/src/Routing/UrlValueConstraint.cs b/src/Components/Components/src/Routing/UrlValueConstraint.cs new file mode 100644 index 000000000000..2cb97915dc16 --- /dev/null +++ b/src/Components/Components/src/Routing/UrlValueConstraint.cs @@ -0,0 +1,176 @@ +// 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 System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.Routing +{ + /// + /// Shared logic for parsing tokens from route values and querystring values. + /// + internal abstract class UrlValueConstraint + { + public delegate bool TryParseDelegate(ReadOnlySpan str, [MaybeNullWhen(false)] out T result); + + private static readonly ConcurrentDictionary _cachedInstances = new(); + + public static bool TryGetByTargetType(Type targetType, [MaybeNullWhen(false)] out UrlValueConstraint result) + { + if (!_cachedInstances.TryGetValue(targetType, out result)) + { + result = Create(targetType); + if (result is null) + { + return false; + } + + _cachedInstances.TryAdd(targetType, result); + } + + return true; + } + + private static bool TryParse(ReadOnlySpan str, out string result) + { + result = str.ToString(); + return true; + } + + private static bool TryParse(ReadOnlySpan str, out DateTime result) + => DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result); + + private static bool TryParse(ReadOnlySpan str, out decimal result) + => decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(ReadOnlySpan str, out double result) + => double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(ReadOnlySpan str, out float result) + => float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(ReadOnlySpan str, out int result) + => int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(ReadOnlySpan str, out long result) + => long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + + private static UrlValueConstraint? Create(Type targetType) => targetType switch + { + var x when x == typeof(string) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(bool) => new TypedUrlValueConstraint(bool.TryParse), + var x when x == typeof(bool?) => new NullableTypedUrlValueConstraint(bool.TryParse), + var x when x == typeof(DateTime) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(DateTime?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(decimal) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(decimal?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(double) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(double?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(float) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(float?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(Guid) => new TypedUrlValueConstraint(Guid.TryParse), + var x when x == typeof(Guid?) => new NullableTypedUrlValueConstraint(Guid.TryParse), + var x when x == typeof(int) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(int?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(long) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(long?) => new NullableTypedUrlValueConstraint(TryParse), + var x => null + }; + + public abstract bool TryParse(ReadOnlySpan value, [MaybeNullWhen(false)] out object result); + + public abstract object? Parse(ReadOnlySpan value, string destinationNameForMessage); + + public abstract Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage); + + private class TypedUrlValueConstraint : UrlValueConstraint + { + private readonly TryParseDelegate _parser; + + public TypedUrlValueConstraint(TryParseDelegate parser) + { + _parser = parser; + } + + public override bool TryParse(ReadOnlySpan value, [MaybeNullWhen(false)] out object result) + { + if (_parser(value, out var typedResult)) + { + result = typedResult!; + return true; + } + else + { + result = null; + return false; + } + } + + public override object? Parse(ReadOnlySpan value, string destinationNameForMessage) + { + if (!_parser(value, out var parsedValue)) + { + throw new InvalidOperationException($"Cannot parse the value '{value.ToString()}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); + } + + return parsedValue; + } + + public override Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage) + { + var count = values.Count; + if (count == 0) + { + return Array.Empty(); + } + + var result = new T?[count]; + + for (var i = 0; i < count; i++) + { + if (!_parser(values[i].Span, out result[i])) + { + throw new InvalidOperationException($"Cannot parse the value '{values[i]}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); + } + } + + return result; + } + } + + private sealed class NullableTypedUrlValueConstraint : TypedUrlValueConstraint where T : struct + { + public NullableTypedUrlValueConstraint(TryParseDelegate parser) + : base(SupportNullable(parser)) + { + } + + private static TryParseDelegate SupportNullable(TryParseDelegate parser) + { + return TryParseNullable; + + bool TryParseNullable(ReadOnlySpan value, [MaybeNullWhen(false)] out T? result) + { + if (value.IsEmpty) + { + result = default; + return true; + } + else if (parser(value, out var parsedValue)) + { + result = parsedValue; + return true; + } + else + { + result = default; + return false; + } + } + } + } + } +} diff --git a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs new file mode 100644 index 000000000000..0d3c693212d3 --- /dev/null +++ b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs @@ -0,0 +1,21 @@ +// 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 System; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Indicates that routing components may supply a value for the parameter from the + /// current URL querystring. They may also supply further values if the URL querystring changes. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class SupplyParameterFromQueryAttribute : Attribute + { + /// + /// Gets or sets the name of the querystring parameter. If null, the querystring + /// parameter is assumed to have the same name as the associated property. + /// + public string? Name { get; set; } + } +} diff --git a/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs b/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs new file mode 100644 index 000000000000..3d0306fe990d --- /dev/null +++ b/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs @@ -0,0 +1,513 @@ +// 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 System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Routing +{ + public class QueryParameterValueSupplierTest + { + private class NoQueryParameters : ComponentBase { } + + [Fact] + public void ComponentWithNoQueryParametersHasNoSupplier() + { + Assert.Null(QueryParameterValueSupplier.ForType(typeof(NoQueryParameters))); + } + + private class IgnorableProperties : ComponentBase + { + [Parameter] public string Invalid1 { get; set; } + [SupplyParameterFromQuery] public string Invalid2 { get; set; } + [Parameter, SupplyParameterFromQuery] public string Valid { get; set; } + [Parameter] public object InvalidAndUnsupportedType { get; set; } + } + + [Fact] + public void SuppliesParametersOnlyForPropertiesWithMatchingAttributes() + { + var query = $"?{nameof(IgnorableProperties.Invalid1)}=a&{nameof(IgnorableProperties.Invalid2)}=b&{nameof(IgnorableProperties.Valid)}=c"; + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(IgnorableProperties.Valid), "c")); + } + + private class ValidTypes : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public bool BoolVal { get; set; } + [Parameter, SupplyParameterFromQuery] public DateTime DateTimeVal { get; set; } + [Parameter, SupplyParameterFromQuery] public decimal DecimalVal { get; set; } + [Parameter, SupplyParameterFromQuery] public double DoubleVal { get; set; } + [Parameter, SupplyParameterFromQuery] public float FloatVal { get; set; } + [Parameter, SupplyParameterFromQuery] public Guid GuidVal { get; set; } + [Parameter, SupplyParameterFromQuery] public int IntVal { get; set; } + [Parameter, SupplyParameterFromQuery] public long LongVal { get; set; } + [Parameter, SupplyParameterFromQuery] public string StringVal { get; set; } + + [Parameter, SupplyParameterFromQuery] public bool? NullableBoolVal { get; set; } + [Parameter, SupplyParameterFromQuery] public DateTime? NullableDateTimeVal { get; set; } + [Parameter, SupplyParameterFromQuery] public decimal? NullableDecimalVal { get; set; } + [Parameter, SupplyParameterFromQuery] public double? NullableDoubleVal { get; set; } + [Parameter, SupplyParameterFromQuery] public float? NullableFloatVal { get; set; } + [Parameter, SupplyParameterFromQuery] public Guid? NullableGuidVal { get; set; } + [Parameter, SupplyParameterFromQuery] public int? NullableIntVal { get; set; } + [Parameter, SupplyParameterFromQuery] public long? NullableLongVal { get; set; } + } + + [Fact] + public void SupportsExpectedValueTypes() + { + var query = + $"{nameof(ValidTypes.BoolVal)}=true&" + + $"{nameof(ValidTypes.DateTimeVal)}=2020-01-02+03:04:05.678-09:00&" + + $"{nameof(ValidTypes.DecimalVal)}=-1.234&" + + $"{nameof(ValidTypes.DoubleVal)}=-2.345&" + + $"{nameof(ValidTypes.FloatVal)}=-3.456&" + + $"{nameof(ValidTypes.GuidVal)}=9e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"{nameof(ValidTypes.IntVal)}=-54321&" + + $"{nameof(ValidTypes.LongVal)}=-99987654321&" + + $"{nameof(ValidTypes.StringVal)}=Some+string+%26+more&" + + $"{nameof(ValidTypes.NullableBoolVal)}=true&" + + $"{nameof(ValidTypes.NullableDateTimeVal)}=2021-01-02+03:04:05.678Z&" + + $"{nameof(ValidTypes.NullableDecimalVal)}=1.234&" + + $"{nameof(ValidTypes.NullableDoubleVal)}=2.345&" + + $"{nameof(ValidTypes.NullableFloatVal)}=3.456&" + + $"{nameof(ValidTypes.NullableGuidVal)}=1e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"{nameof(ValidTypes.NullableIntVal)}=54321&" + + $"{nameof(ValidTypes.NullableLongVal)}=99987654321&"; + + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(ValidTypes.BoolVal), true), + AssertKeyValuePair(nameof(ValidTypes.DateTimeVal), new DateTimeOffset(2020, 1, 2, 3, 4, 5, 678, TimeSpan.FromHours(-9)).LocalDateTime), + AssertKeyValuePair(nameof(ValidTypes.DecimalVal), -1.234m), + AssertKeyValuePair(nameof(ValidTypes.DoubleVal), -2.345), + AssertKeyValuePair(nameof(ValidTypes.FloatVal), -3.456f), + AssertKeyValuePair(nameof(ValidTypes.GuidVal), new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9")), + AssertKeyValuePair(nameof(ValidTypes.IntVal), -54321), + AssertKeyValuePair(nameof(ValidTypes.LongVal), -99987654321), + AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), true), + AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime()), + AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), 1.234m), + AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), 2.345), + AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), 3.456f), + AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9")), + AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), 54321), + AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), 99987654321), + AssertKeyValuePair(nameof(ValidTypes.StringVal), "Some string & more")); + } + + [Theory] + [InlineData("")] + [InlineData("?")] + [InlineData("?unrelated=123")] + public void SuppliesNullForValueTypesIfNotSpecified(string query) + { + // Although we could supply default(T) for missing values, there's precedent in the routing + // system for supplying null for missing route parameters. The component is then responsible + // for interpreting null as a blank value for the parameter, regardless of its type. To keep + // the rules aligned, we do the same thing for querystring parameters. + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(ValidTypes.BoolVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.DateTimeVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.DecimalVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.DoubleVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.FloatVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.GuidVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.IntVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.LongVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.StringVal), (object)null)); + } + + private class ValidArrayTypes : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public bool[] BoolVals { get; set; } + [Parameter, SupplyParameterFromQuery] public DateTime[] DateTimeVals { get; set; } + [Parameter, SupplyParameterFromQuery] public decimal[] DecimalVals { get; set; } + [Parameter, SupplyParameterFromQuery] public double[] DoubleVals { get; set; } + [Parameter, SupplyParameterFromQuery] public float[] FloatVals { get; set; } + [Parameter, SupplyParameterFromQuery] public Guid[] GuidVals { get; set; } + [Parameter, SupplyParameterFromQuery] public int[] IntVals { get; set; } + [Parameter, SupplyParameterFromQuery] public long[] LongVals { get; set; } + [Parameter, SupplyParameterFromQuery] public string[] StringVals { get; set; } + + [Parameter, SupplyParameterFromQuery] public bool?[] NullableBoolVals { get; set; } + [Parameter, SupplyParameterFromQuery] public DateTime?[] NullableDateTimeVals { get; set; } + [Parameter, SupplyParameterFromQuery] public decimal?[] NullableDecimalVals { get; set; } + [Parameter, SupplyParameterFromQuery] public double?[] NullableDoubleVals { get; set; } + [Parameter, SupplyParameterFromQuery] public float?[] NullableFloatVals { get; set; } + [Parameter, SupplyParameterFromQuery] public Guid?[] NullableGuidVals { get; set; } + [Parameter, SupplyParameterFromQuery] public int?[] NullableIntVals { get; set; } + [Parameter, SupplyParameterFromQuery] public long?[] NullableLongVals { get; set; } + } + + [Fact] + public void SupportsExpectedArrayTypes() + { + var query = + $"{nameof(ValidArrayTypes.BoolVals)}=true&" + + $"{nameof(ValidArrayTypes.DateTimeVals)}=2020-01-02+03:04:05.678Z&" + + $"{nameof(ValidArrayTypes.DecimalVals)}=-1.234&" + + $"{nameof(ValidArrayTypes.DoubleVals)}=-2.345&" + + $"{nameof(ValidArrayTypes.FloatVals)}=-3.456&" + + $"{nameof(ValidArrayTypes.GuidVals)}=9e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"{nameof(ValidArrayTypes.IntVals)}=-54321&" + + $"{nameof(ValidArrayTypes.LongVals)}=-99987654321&" + + $"{nameof(ValidArrayTypes.StringVals)}=Some+string+%26+more&" + + $"{nameof(ValidArrayTypes.NullableBoolVals)}=true&" + + $"{nameof(ValidArrayTypes.NullableDateTimeVals)}=2021-01-02+03:04:05.678Z&" + + $"{nameof(ValidArrayTypes.NullableDecimalVals)}=1.234&" + + $"{nameof(ValidArrayTypes.NullableDoubleVals)}=2.345&" + + $"{nameof(ValidArrayTypes.NullableFloatVals)}=3.456&" + + $"{nameof(ValidArrayTypes.NullableGuidVals)}=1e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"{nameof(ValidArrayTypes.NullableIntVals)}=54321&" + + $"{nameof(ValidArrayTypes.NullableLongVals)}=99987654321&"; + + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(ValidArrayTypes.BoolVals), new[] { true }), + AssertKeyValuePair(nameof(ValidArrayTypes.DateTimeVals), new[] { new DateTime(2020, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }), + AssertKeyValuePair(nameof(ValidArrayTypes.DecimalVals), new[] { -1.234m }), + AssertKeyValuePair(nameof(ValidArrayTypes.DoubleVals), new[] { -2.345 }), + AssertKeyValuePair(nameof(ValidArrayTypes.FloatVals), new[] { -3.456f }), + AssertKeyValuePair(nameof(ValidArrayTypes.GuidVals), new[] { new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9") }), + AssertKeyValuePair(nameof(ValidArrayTypes.IntVals), new[] { -54321 }), + AssertKeyValuePair(nameof(ValidArrayTypes.LongVals), new[] { -99987654321 }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), new[] { true }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), new[] { new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), new[] { 1.234m }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), new[] { 2.345 }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), new[] { 3.456f }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), new[] { new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9") }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), new[] { 54321 }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), new[] { 99987654321 }), + AssertKeyValuePair(nameof(ValidArrayTypes.StringVals), new[] { "Some string & more" })); + } + + [Theory] + [InlineData("")] + [InlineData("?")] + [InlineData("?unrelated=123")] + public void SuppliesEmptyArrayForArrayTypesIfNotSpecified(string query) + { + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(ValidArrayTypes.BoolVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.DateTimeVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.DecimalVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.DoubleVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.FloatVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.GuidVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.IntVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.LongVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), Array.Empty()), + AssertKeyValuePair(nameof(ValidArrayTypes.StringVals), Array.Empty())); + } + + class OverrideParameterName : ComponentBase + { + [Parameter, SupplyParameterFromQuery(Name = "anothername1")] public string Value1 { get; set; } + [Parameter, SupplyParameterFromQuery(Name = "anothername2")] public string Value2 { get; set; } + } + + [Fact] + public void CanOverrideParameterName() + { + var query = $"anothername1=Some+value+1&Value2=Some+value+2"; + Assert.Collection(GetSuppliedParameters(query), + // Because we specified the mapped name, we receive the value + AssertKeyValuePair(nameof(OverrideParameterName.Value1), "Some value 1"), + // If we specify the component parameter name directly, we do not receive the value + AssertKeyValuePair(nameof(OverrideParameterName.Value2), (object)null)); + } + + class MapSingleQueryParameterToMultipleProperties : ComponentBase + { + [Parameter, SupplyParameterFromQuery(Name = "a")] public int ValueAsInt { get; set; } + [Parameter, SupplyParameterFromQuery(Name = "b")] public DateTime ValueAsDateTime { get; set; } + [Parameter, SupplyParameterFromQuery(Name = "A")] public long ValueAsLong { get; set; } + } + + [Fact] + public void CannotMapSingleQueryParameterToMultipleProperties() + { + var ex = Assert.Throws( + () => QueryParameterValueSupplier.ForType(typeof(MapSingleQueryParameterToMultipleProperties))); + Assert.Contains("declares more than one mapping for the query parameter 'a'.", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + class UnsupportedType : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public int IntValid { get; set; } + [Parameter, SupplyParameterFromQuery] public object ObjectValue { get; set; } + } + + [Fact] + public void RejectsUnsupportedType() + { + var ex = Assert.Throws( + () => QueryParameterValueSupplier.ForType(typeof(UnsupportedType))); + Assert.Equal("Querystring values cannot be parsed as type 'System.Object'.", ex.Message); + } + + [Theory] + [InlineData(nameof(ValidTypes.BoolVal), "abc", typeof(bool))] + [InlineData(nameof(ValidTypes.DateTimeVal), "2020-02-31", typeof(DateTime))] + [InlineData(nameof(ValidTypes.DecimalVal), "1.2.3", typeof(decimal))] + [InlineData(nameof(ValidTypes.DoubleVal), "1x", typeof(double))] + [InlineData(nameof(ValidTypes.FloatVal), "1e1000", typeof(float))] + [InlineData(nameof(ValidTypes.GuidVal), "123456-789-0", typeof(Guid))] + [InlineData(nameof(ValidTypes.IntVal), "5000000000", typeof(int))] + [InlineData(nameof(ValidTypes.LongVal), "this+is+a+long+value", typeof(long))] + [InlineData(nameof(ValidTypes.NullableBoolVal), "abc", typeof(bool?))] + [InlineData(nameof(ValidTypes.NullableDateTimeVal), "2020-02-31", typeof(DateTime?))] + [InlineData(nameof(ValidTypes.NullableDecimalVal), "1.2.3", typeof(decimal?))] + [InlineData(nameof(ValidTypes.NullableDoubleVal), "1x", typeof(double?))] + [InlineData(nameof(ValidTypes.NullableFloatVal), "1e1000", typeof(float?))] + [InlineData(nameof(ValidTypes.NullableGuidVal), "123456-789-0", typeof(Guid?))] + [InlineData(nameof(ValidTypes.NullableIntVal), "5000000000", typeof(int?))] + [InlineData(nameof(ValidTypes.NullableLongVal), "this+is+a+long+value", typeof(long?))] + public void RejectsUnparseableValues(string key, string value, Type targetType) + { + var ex = Assert.Throws( + () => GetSuppliedParameters($"?{key}={value}")); + Assert.Equal($"Cannot parse the value '{value.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Theory] + [InlineData(nameof(ValidArrayTypes.BoolVals), "true", "abc", typeof(bool))] + [InlineData(nameof(ValidArrayTypes.DateTimeVals), "2020-02-28", "2020-02-31", typeof(DateTime))] + [InlineData(nameof(ValidArrayTypes.DecimalVals), "1.23", "1.2.3", typeof(decimal))] + [InlineData(nameof(ValidArrayTypes.DoubleVals), "1", "1x", typeof(double))] + [InlineData(nameof(ValidArrayTypes.FloatVals), "1000", "1e1000", typeof(float))] + [InlineData(nameof(ValidArrayTypes.GuidVals), "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid))] + [InlineData(nameof(ValidArrayTypes.IntVals), "5000000", "5000000000", typeof(int))] + [InlineData(nameof(ValidArrayTypes.LongVals), "-1234", "this+is+a+long+value", typeof(long))] + [InlineData(nameof(ValidArrayTypes.NullableBoolVals), "true", "abc", typeof(bool?))] + [InlineData(nameof(ValidArrayTypes.NullableDateTimeVals), "2020-02-28", "2020-02-31", typeof(DateTime?))] + [InlineData(nameof(ValidArrayTypes.NullableDecimalVals), "1.23", "1.2.3", typeof(decimal?))] + [InlineData(nameof(ValidArrayTypes.NullableDoubleVals), "1", "1x", typeof(double?))] + [InlineData(nameof(ValidArrayTypes.NullableFloatVals), "1000", "1e1000", typeof(float?))] + [InlineData(nameof(ValidArrayTypes.NullableGuidVals), "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid?))] + [InlineData(nameof(ValidArrayTypes.NullableIntVals), "5000000", "5000000000", typeof(int?))] + [InlineData(nameof(ValidArrayTypes.NullableLongVals), "-1234", "this+is+a+long+value", typeof(long?))] + public void RejectsUnparseableArrayEntries(string key, string validValue, string invalidValue, Type targetType) + { + var ex = Assert.Throws( + () => GetSuppliedParameters($"?{key}={validValue}&{key}={invalidValue}")); + Assert.Equal($"Cannot parse the value '{invalidValue.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Theory] + [InlineData(nameof(ValidTypes.BoolVal), typeof(bool))] + [InlineData(nameof(ValidTypes.DateTimeVal), typeof(DateTime))] + [InlineData(nameof(ValidTypes.DecimalVal), typeof(decimal))] + [InlineData(nameof(ValidTypes.DoubleVal), typeof(double))] + [InlineData(nameof(ValidTypes.FloatVal), typeof(float))] + [InlineData(nameof(ValidTypes.GuidVal), typeof(Guid))] + [InlineData(nameof(ValidTypes.IntVal), typeof(int))] + [InlineData(nameof(ValidTypes.LongVal), typeof(long))] + public void RejectsBlankValuesWhenNotNullable(string key, Type targetType) + { + var ex = Assert.Throws( + () => GetSuppliedParameters($"?{nameof(ValidTypes.StringVal)}=somevalue&{key}=")); + Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Fact] + public void AcceptsBlankValuesWhenNullable() + { + var query = + $"{nameof(ValidTypes.NullableBoolVal)}=&" + + $"{nameof(ValidTypes.NullableDateTimeVal)}=&" + + $"{nameof(ValidTypes.NullableDecimalVal)}=&" + + $"{nameof(ValidTypes.NullableDoubleVal)}=&" + + $"{nameof(ValidTypes.NullableFloatVal)}=&" + + $"{nameof(ValidTypes.NullableGuidVal)}=&" + + $"{nameof(ValidTypes.NullableIntVal)}=&" + + $"{nameof(ValidTypes.NullableLongVal)}=&"; + Assert.Collection(GetSuppliedParameters(query).Where(pair => pair.key.StartsWith("Nullable", StringComparison.Ordinal)), + AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), (object)null), + AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), (object)null)); + } + + [Theory] + [InlineData("")] + [InlineData("=")] + public void EmptyStringValuesAreSuppliedAsEmptyString(string queryPart) + { + var query = $"?{nameof(ValidTypes.StringVal)}{queryPart}"; + var suppliedParameters = GetSuppliedParameters(query).ToDictionary(x => x.key, x => x.value); + Assert.Equal(string.Empty, suppliedParameters[nameof(ValidTypes.StringVal)]); + } + + [Fact] + public void EmptyStringArrayValuesAreSuppliedAsEmptyStrings() + { + var query = $"?{nameof(ValidArrayTypes.StringVals)}=a&" + + $"{nameof(ValidArrayTypes.StringVals)}&" + + $"{nameof(ValidArrayTypes.StringVals)}=&" + + $"{nameof(ValidArrayTypes.StringVals)}=b"; + var suppliedParameters = GetSuppliedParameters(query).ToDictionary(x => x.key, x => x.value); + Assert.Equal(new[] { "a", string.Empty, string.Empty, "b" }, suppliedParameters[nameof(ValidArrayTypes.StringVals)]); + } + + [Theory] + [InlineData(nameof(ValidArrayTypes.BoolVals), typeof(bool))] + [InlineData(nameof(ValidArrayTypes.DateTimeVals), typeof(DateTime))] + [InlineData(nameof(ValidArrayTypes.DecimalVals), typeof(decimal))] + [InlineData(nameof(ValidArrayTypes.DoubleVals), typeof(double))] + [InlineData(nameof(ValidArrayTypes.FloatVals), typeof(float))] + [InlineData(nameof(ValidArrayTypes.GuidVals), typeof(Guid))] + [InlineData(nameof(ValidArrayTypes.IntVals), typeof(int))] + [InlineData(nameof(ValidArrayTypes.LongVals), typeof(long))] + public void RejectsBlankArrayEntriesWhenNotNullable(string key, Type targetType) + { + var ex = Assert.Throws( + () => GetSuppliedParameters($"?{nameof(ValidTypes.StringVal)}=somevalue&{key}=")); + Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Fact] + public void AcceptsBlankArrayEntriesWhenNullable() + { + var query = + $"{nameof(ValidArrayTypes.NullableBoolVals)}=&" + + $"{nameof(ValidArrayTypes.NullableDateTimeVals)}=&" + + $"{nameof(ValidArrayTypes.NullableDecimalVals)}=&" + + $"{nameof(ValidArrayTypes.NullableDoubleVals)}=&" + + $"{nameof(ValidArrayTypes.NullableFloatVals)}=&" + + $"{nameof(ValidArrayTypes.NullableGuidVals)}=&" + + $"{nameof(ValidArrayTypes.NullableIntVals)}=&" + + $"{nameof(ValidArrayTypes.NullableLongVals)}=&"; + Assert.Collection(GetSuppliedParameters(query).Where(pair => pair.key.StartsWith("Nullable", StringComparison.Ordinal)), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), new bool?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), new DateTime?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), new decimal?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), new double?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), new float?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), new Guid?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), new int?[] { null }), + AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), new long?[] { null })); + } + + private class SpecialQueryParameterName : ComponentBase + { + public const string NameThatLooksEncoded = "name+that+looks+%5Bencoded%5D"; + [Parameter, SupplyParameterFromQuery(Name = NameThatLooksEncoded)] public string Key { get; set; } + } + + [Fact] + public void DecodesKeysAndValues() + { + var encodedName = Uri.EscapeDataString(SpecialQueryParameterName.NameThatLooksEncoded); + var query = $"?{encodedName}=Some+%5Bencoded%5D+value"; + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(SpecialQueryParameterName.Key), "Some [encoded] value")); + } + + private class KeyCaseMatching : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public int KeyOne { get; set; } + [Parameter, SupplyParameterFromQuery(Name = "keytwo")] public int KeyTwo { get; set; } + } + + [Fact] + public void MatchesKeysCaseInsensitively() + { + var query = $"?KEYONE=1&KEYTWO=2"; + Assert.Collection(GetSuppliedParameters(query), + AssertKeyValuePair(nameof(KeyCaseMatching.KeyOne), 1), + AssertKeyValuePair(nameof(KeyCaseMatching.KeyTwo), 2)); + } + + private class KeysWithNonAsciiChars : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public string Имя_моей_собственности { get; set; } + [Parameter, SupplyParameterFromQuery(Name = "خاصية_أخرى")] public string AnotherProperty { get; set; } + } + + [Fact] + public void MatchesKeysWithNonAsciiChars() + { + var query = $"?{nameof(KeysWithNonAsciiChars.Имя_моей_собственности)}=first&خاصية_أخرى=second"; + var result = GetSuppliedParameters(query); + Assert.Collection(result, + AssertKeyValuePair(nameof(KeysWithNonAsciiChars.AnotherProperty), "second"), + AssertKeyValuePair(nameof(KeysWithNonAsciiChars.Имя_моей_собственности), "first")); + } + + private class SingleValueOverwriting : ComponentBase + { + [Parameter, SupplyParameterFromQuery] public int Age { get; set; } + [Parameter, SupplyParameterFromQuery] public int? Id { get; set; } + [Parameter, SupplyParameterFromQuery] public string Name { get; set; } + } + + [Fact] + public void ForNonArrayValuesOnlyOneValueIsSupplied() + { + // For simplicity and speed, the value assignment logic doesn't check if the a single-valued destination is + // already populated, and just overwrites in a left-to-right manner. For nullable values it's possible to + // overwrite a value with null, or a string with empty. + Assert.Collection(GetSuppliedParameters($"?age=123&age=456&age=789&id=1&id&name=Bobbins&name"), + AssertKeyValuePair(nameof(SingleValueOverwriting.Age), 789), + AssertKeyValuePair(nameof(SingleValueOverwriting.Id), (int?)null), + AssertKeyValuePair(nameof(SingleValueOverwriting.Name), string.Empty)); + } + + private static IEnumerable<(string key, object value)> GetSuppliedParameters(string query) where TComponent : IComponent + { + var supplier = QueryParameterValueSupplier.ForType(typeof(TComponent)); + using var builder = new RenderTreeBuilder(); + builder.OpenComponent(0); + supplier.RenderParametersFromQueryString(builder, query.AsMemory()); + builder.CloseComponent(); + + var frames = builder.GetFrames(); + return frames.Array.Take(frames.Count) + .Where(frame => frame.FrameType == RenderTree.RenderTreeFrameType.Attribute) + .Select(frame => (frame.AttributeName, frame.AttributeValue)) + .OrderBy(pair => pair.AttributeName) // The order isn't defined, so use alphabetical for tests + .ToList(); + } + + private Action<(string key, object value)> AssertKeyValuePair(string expectedKey, T expectedValue) + { + return pair => + { + Assert.Equal(expectedKey, pair.key); + if (expectedValue is null) + { + Assert.Null(pair.value); + } + else + { + Assert.IsType(expectedValue); + Assert.Equal(expectedValue, pair.value); + } + }; + } + } +} diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs index 30cc1d2832e6..b97a1750c841 100644 --- a/src/Components/test/E2ETest/Tests/RoutingTest.cs +++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs @@ -100,7 +100,7 @@ public void CanArriveAtPageWithOptionalParametersProvided() [Fact] public void CanArriveAtPageWithOptionalParametersNotProvided() { - SetUrlViaPushState($"/WithOptionalParameters"); + SetUrlViaPushState($"/WithOptionalParameters?query=ignored"); var app = Browser.MountTestComponent(); var expected = $"Your age is ."; @@ -111,7 +111,7 @@ public void CanArriveAtPageWithOptionalParametersNotProvided() [Fact] public void CanArriveAtPageWithCatchAllParameter() { - SetUrlViaPushState("/WithCatchAllParameter/life/the/universe/and/everything%20%3D%2042"); + SetUrlViaPushState("/WithCatchAllParameter/life/the/universe/and/everything%20%3D%2042?query=ignored"); var app = Browser.MountTestComponent(); var expected = $"The answer: life/the/universe/and/everything = 42."; @@ -768,6 +768,91 @@ IWebElement GetFocusedElement() => Browser.SwitchTo().ActiveElement(); } + [Fact] + public void CanArriveAtQueryStringPageWithNoQuery() + { + SetUrlViaPushState("/WithQueryParameters/Abc"); + + var app = Browser.MountTestComponent(); + Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + + AssertHighlightedLinks("With query parameters (none)"); + } + + [Fact] + public void CanArriveAtQueryStringPageWithQuery() + { + SetUrlViaPushState("/WithQueryParameters/Abc?stringvalue=Hello+there"); + + var app = Browser.MountTestComponent(); + Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + + AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)"); + } + + [Fact] + public void CanNavigateToQueryStringPageWithNoQuery() + { + SetUrlViaPushState("/"); + + var app = Browser.MountTestComponent(); + app.FindElement(By.LinkText("With query parameters (none)")).Click(); + + Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + + AssertHighlightedLinks("With query parameters (none)"); + } + + [Fact] + public void CanNavigateBetweenPagesWithQueryStrings() + { + SetUrlViaPushState("/"); + + // Navigate to a page with querystring + var app = Browser.MountTestComponent(); + app.FindElement(By.LinkText("With query parameters (passing string value)")).Click(); + + Browser.Equal("Hello Abc .", () => app.FindElement(By.Id("test-info")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + var instanceId = app.FindElement(By.Id("instance-id")).Text; + Assert.True(!string.IsNullOrWhiteSpace(instanceId)); + + AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)"); + + // We can also navigate to a different query while retaining the same component instance + app.FindElement(By.LinkText("With IntValue and LongValues")).Click(); + Browser.Equal("123", () => app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("3 values (50, 100, -20)", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal(instanceId, app.FindElement(By.Id("instance-id")).Text); + AssertHighlightedLinks("With query parameters (none)"); + + // We can also click back to go the preceding query while retaining the same component instance + Browser.Navigate().Back(); + Browser.Equal("0", () => app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); + Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal(instanceId, app.FindElement(By.Id("instance-id")).Text); + AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)"); + } + private long BrowserScrollY { get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY"); diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/Links.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/Links.razor index ac7a095f1ba4..ed381f72f8f2 100644 --- a/src/Components/test/testassets/BasicTestApp/RouterTest/Links.razor +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/Links.razor @@ -18,6 +18,8 @@
  • Other with hash
  • With parameters
  • With more parameters
  • +
  • With query parameters (none)
  • +
  • With query parameters (passing string value)
  • Long page 1
  • Long page 2
  • With lazy assembly
  • diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor new file mode 100644 index 000000000000..36e3fd058634 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor @@ -0,0 +1,32 @@ +@page "/WithQueryParameters/{firstName}/{OptionalLastName?}" +Hello @FirstName @OptionalLastName. +

    IntValue: @IntValue

    +

    NullableDateTimeValue: @NullableDateTimeValue?.ToString("hh:mm:ss on yyyy-MM-dd")

    +

    StringValue: @StringValue

    +

    LongValues: @LongValues.Length values (@string.Join(", ", LongValues.Select(x => x.ToString()).ToArray()))

    + +

    Instance ID: @instanceId

    + +

    + Links: + With IntValue | + With NullableDateTimeValue | + With IntValue and LongValues | +

    + +@code +{ + private string instanceId = Guid.NewGuid().ToString(); + + [Parameter] public string FirstName { get; set; } + + [Parameter] public string OptionalLastName { get ; set; } + + [Parameter, SupplyParameterFromQuery] public int IntValue { get ; set; } + + [Parameter, SupplyParameterFromQuery] public DateTime? NullableDateTimeValue { get ; set; } + + [Parameter, SupplyParameterFromQuery] public string StringValue { get ; set; } + + [Parameter, SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } +} From 60b90db5c5bcc605c23017e6d07844c7d67bdec0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 15:23:32 -0700 Subject: [PATCH 13/15] [main] (deps): Bump src/submodules/googletest (#34096) Bumps [src/submodules/googletest](https://github.com/google/googletest) from `e2239ee` to `4ec4cd2`. - [Release notes](https://github.com/google/googletest/releases) - [Commits](https://github.com/google/googletest/compare/e2239ee6043f73722e7aa812a459f54a28552929...4ec4cd23f486bf70efcc5d2caa40f24368f752e3) --- updated-dependencies: - dependency-name: src/submodules/googletest dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/submodules/googletest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/submodules/googletest b/src/submodules/googletest index e2239ee6043f..4ec4cd23f486 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit e2239ee6043f73722e7aa812a459f54a28552929 +Subproject commit 4ec4cd23f486bf70efcc5d2caa40f24368f752e3 From e65cec17d41f5563f2c5dde35422741f11e3fabb Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 6 Jul 2021 09:19:20 -0700 Subject: [PATCH 14/15] Support for 'multiple' attribute in ' is special, in that anything we write to .value will be lost if there // isn't yet a matching
    public class InputSelect<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase { + private readonly bool _isMultipleSelect; + + /// + /// Constructs an instance of . + /// + public InputSelect() + { + _isMultipleSelect = typeof(TValue).IsArray; + } + /// /// Gets or sets the child content to be rendering inside the select element. /// @@ -30,15 +42,33 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.OpenElement(0, "select"); builder.AddMultipleAttributes(1, AdditionalAttributes); builder.AddAttribute(2, "class", CssClass); - builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString)); - builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); - builder.AddElementReferenceCapture(5, __selectReference => Element = __selectReference); - builder.AddContent(6, ChildContent); + builder.AddAttribute(3, "multiple", _isMultipleSelect); + + if (_isMultipleSelect) + { + builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValue)?.ToString()); + builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder(this, SetCurrentValueAsStringArray, default)); + } + else + { + builder.AddAttribute(6, "value", CurrentValueAsString); + builder.AddAttribute(7, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, default)); + } + + builder.AddElementReferenceCapture(8, __selectReference => Element = __selectReference); + builder.AddContent(9, ChildContent); builder.CloseElement(); } /// protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage); + + private void SetCurrentValueAsStringArray(string?[]? value) + { + CurrentValue = BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var result) + ? result + : default; + } } } diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 5440602e4141..235000da0d1e 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -271,6 +271,67 @@ public void InputSelectInteractsWithEditContext() Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor); } + [Fact] + public void InputSelectInteractsWithEditContext_MultipleAttribute() + { + var appElement = MountTypicalValidationComponent(); + var citiesInput = new SelectElement(appElement.FindElement(By.ClassName("cities")).FindElement(By.TagName("select"))); + var select = citiesInput.WrappedElement; + var messagesAccesor = CreateValidationMessagesAccessor(appElement); + + // Binding applies to option selection + Browser.Equal(new[] { "SanFrancisco" }, () => citiesInput.AllSelectedOptions.Select(option => option.GetAttribute("value"))); + + // Validates on edit + Browser.Equal("valid", () => select.GetAttribute("class")); + citiesInput.SelectByIndex(2); + Browser.Equal("modified valid", () => select.GetAttribute("class")); + + // Can become invalid + citiesInput.SelectByIndex(1); + citiesInput.SelectByIndex(3); + Browser.Equal("modified invalid", () => select.GetAttribute("class")); + Browser.Equal(new[] { "The field SelectedCities must be a string or array type with a maximum length of '3'." }, messagesAccesor); + } + + [Fact] + public void InputSelectIgnoresMultipleAttribute() + { + var appElement = MountTypicalValidationComponent(); + var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select"))); + var select = ticketClassInput.WrappedElement; + + // Select does not have the 'multiple' attribute + Browser.False(() => ticketClassInput.IsMultiple); + + // Check initial selection + Browser.Equal("Economy class", () => ticketClassInput.SelectedOption.Text); + + ticketClassInput.SelectByText("First class"); + + // Only one option selected + Browser.Equal(1, () => ticketClassInput.AllSelectedOptions.Count); + } + + [Fact] + public void InputSelectHandlesHostileStringValues() + { + var appElement = MountTypicalValidationComponent(); + var selectParagraph = appElement.FindElement(By.ClassName("select-multiple-hostile")); + var hostileSelectInput = new SelectElement(selectParagraph.FindElement(By.TagName("select"))); + var select = hostileSelectInput.WrappedElement; + var hostileSelectLabel = selectParagraph.FindElement(By.TagName("span")); + + // Check initial selection + Browser.Equal(new[] { "\"", "{" }, () => hostileSelectInput.AllSelectedOptions.Select(o => o.Text)); + + hostileSelectInput.DeselectByIndex(0); + hostileSelectInput.SelectByIndex(2); + + // Bindings work from JS -> C# + Browser.Equal("{,", () => hostileSelectLabel.Text); + } + [Fact] public void InputCheckboxInteractsWithEditContext() { @@ -543,6 +604,40 @@ public void SelectComponentSupportsOptionsComponent() Browser.Equal("", () => selectWithoutComponent.GetAttribute("value")); } + [Fact] + public void SelectWithMultipleAttributeCanBindValue() + { + var appElement = Browser.MountTestComponent(); + var select = new SelectElement(appElement.FindElement(By.Id("select-cities"))); + + // Assert that the binding works in the .NET -> JS direction + Browser.Equal(new[] { "\"sf\"", "\"sea\"" }, () => select.AllSelectedOptions.Select(option => option.GetAttribute("value"))); + + select.DeselectByIndex(0); + select.SelectByIndex(1); + select.SelectByIndex(2); + + var label = appElement.FindElement(By.Id("selected-cities-label")); + + // Assert that the binding works in the JS -> .NET direction + Browser.Equal("\"la\", \"pdx\", \"sea\"", () => label.Text); + } + + [Fact] + public void SelectWithMultipleAttributeCanUseOnChangedCallback() + { + var appElement = Browser.MountTestComponent(); + var select = new SelectElement(appElement.FindElement(By.Id("select-cars"))); + + select.SelectByIndex(2); + select.SelectByIndex(3); + + var label = appElement.FindElement(By.Id("selected-cars-label")); + + // Assert that the callback was invoked and the selected options were correctly passed. + Browser.Equal("opel, audi", () => label.Text); + } + [Fact] public void RespectsCustomFieldCssClassProvider() { diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor index c6be1322d73c..b3c9436a30a3 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor @@ -35,7 +35,8 @@

    Ticket class: - + @*We specify 'multiple' here, but it has no effect since we are not binding to an array type.*@ + @@ -43,6 +44,23 @@ @person.TicketClass

    +

    + @*Here, the 'multiple' attribute is inferred because we are binding to an array type.*@ + + + + + + +

    +

    + + + + + + @string.Join(", ", person.HostileStrings) +

    Airline: @@ -158,6 +176,12 @@ [Required, StringLength(10), CustomValidationClassName(Valid = "valid-socks", Invalid = "invalid-socks")] public string SocksColor { get; set; } + [Required, MinLength(2), MaxLength(3)] + public City[] SelectedCities { get; set; } = new[] { City.SanFrancisco }; + + [Required] + public string[] HostileStrings { get; set; } = new string[] { "\"", "{" }; + public string Username { get; set; } } @@ -169,6 +193,8 @@ enum Country { Japan, Yemen, Latvia } + enum City { SanFrancisco, Tokyo, London, Madrid } + List submissionLog = new List(); // So we can assert about the callbacks void HandleValidSubmit() diff --git a/src/Components/test/testassets/BasicTestApp/SelectVariantsComponent.razor b/src/Components/test/testassets/BasicTestApp/SelectVariantsComponent.razor index 7573b62fb305..1b79557168b5 100644 --- a/src/Components/test/testassets/BasicTestApp/SelectVariantsComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/SelectVariantsComponent.razor @@ -20,14 +20,39 @@ } + + +@string.Join(", ", SelectedCars) + + + +@string.Join(", ", SelectedCities) + @code { public string SelectValue { get; set; } = "B"; + public string[] SelectedCities { get; set; } = new[] { "\"sf\"", "\"sea\"" }; + public string[] SelectedCars { get; set; } = new string[] { }; public bool ShowAdditionalOption = false; void ToggleShowAdditionalOption() { ShowAdditionalOption = true; } + + void SelectedCarsChanged(ChangeEventArgs e) + { + SelectedCars = (string[])e.Value; + } } From 051aa954793484f702f55541d95b99146ce35263 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 6 Jul 2021 09:44:29 -0700 Subject: [PATCH 15/15] Kestrel response header encoding (#33776) --- .../Internal/Http/HttpHeaders.Generated.cs | 398 +++++++++--------- .../Core/src/Internal/Http/HttpHeaders.cs | 13 +- .../Core/src/Internal/Http/HttpProtocol.cs | 1 + .../src/Internal/Http/HttpRequestHeaders.cs | 6 +- .../src/Internal/Http/HttpResponseHeaders.cs | 49 ++- .../src/Internal/Http/HttpResponseTrailers.cs | 8 + .../src/Internal/Http2/HPackHeaderWriter.cs | 6 +- .../src/Internal/Http2/Http2FrameWriter.cs | 18 +- ...numerator.cs => Http2HeadersEnumerator.cs} | 10 +- .../Http2/Http2Stream.FeatureCollection.cs | 2 +- .../src/Internal/Http3/Http3FrameWriter.cs | 32 +- .../Internal/Http3/Http3HeadersEnumerator.cs | 152 +++++++ .../Http3/Http3Stream.FeatureCollection.cs | 2 +- .../src/Internal/Http3/QPackHeaderWriter.cs | 13 +- .../Internal/Infrastructure/HttpCharacters.cs | 21 +- .../Internal/Infrastructure/HttpUtilities.cs | 2 +- .../Internal/Infrastructure/IKestrelTrace.cs | 8 +- .../Internal/Infrastructure/KestrelTrace.cs | 8 +- .../Kestrel/Core/src/KestrelServerOptions.cs | 16 +- .../Kestrel/Core/src/PublicAPI.Unshipped.txt | 2 + .../Core/test/Http2HPackEncoderTests.cs | 162 +++++++ .../Core/test/Http2HeadersEnumeratorTests.cs | 1 - .../Core/test/Http3HeadersEnumeratorTests.cs | 156 +++++++ .../Core/test/HttpResponseHeadersTests.cs | 112 +++++ .../Core/test/PipelineExtensionTests.cs | 3 +- src/Servers/Kestrel/Core/test/UTF8Decoding.cs | 4 +- .../Microbenchmarks/BytesToStringBenchmark.cs | 2 +- .../HPackHeaderWriterBenchmark.cs | 45 +- .../perf/Microbenchmarks/Mocks/MockTrace.cs | 8 +- src/Servers/Kestrel/shared/KnownHeaders.cs | 37 +- .../shared/test/CompositeKestrelTrace.cs | 8 +- .../shared/test/StreamBackedTestConnection.cs | 4 +- .../Http2/Http2StreamTests.cs | 208 +++++++++ .../Http3/Http3StreamTests.cs | 197 +++++++++ .../Http3/Http3TestBase.cs | 15 +- .../ResponseHeaderTests.cs | 104 +++++ .../TestTransport/InMemoryConnection.cs | 5 +- .../TestTransport/TestServer.cs | 5 +- src/Shared/Hpack/DynamicHPackEncoder.cs | 38 +- src/Shared/Hpack/EncoderHeaderEntry.cs | 10 +- src/Shared/Http2cat/HPackHeaderWriter.cs | 2 +- .../ServerInfrastructure/BufferExtensions.cs | 57 ++- .../runtime/Http2/Hpack/HPackEncoder.cs | 30 +- src/Shared/runtime/Http2/Hpack/HeaderField.cs | 2 +- 44 files changed, 1632 insertions(+), 350 deletions(-) rename src/Servers/Kestrel/Core/src/Internal/Http2/{Http2HeaderEnumerator.cs => Http2HeadersEnumerator.cs} (96%) create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs create mode 100644 src/Servers/Kestrel/Core/test/Http3HeadersEnumeratorTests.cs create mode 100644 src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseHeaderTests.cs diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs index b335d7837694..1b265ee474d7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -9,6 +9,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.Http; @@ -1567,7 +1568,6 @@ StringValues IHeaderDictionary.AcceptRanges set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AcceptRanges, value); } } @@ -1585,7 +1585,6 @@ StringValues IHeaderDictionary.AccessControlAllowCredentials set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlAllowCredentials, value); } } @@ -1603,7 +1602,6 @@ StringValues IHeaderDictionary.AccessControlAllowHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlAllowHeaders, value); } } @@ -1621,7 +1619,6 @@ StringValues IHeaderDictionary.AccessControlAllowMethods set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlAllowMethods, value); } } @@ -1639,7 +1636,6 @@ StringValues IHeaderDictionary.AccessControlAllowOrigin set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlAllowOrigin, value); } } @@ -1657,7 +1653,6 @@ StringValues IHeaderDictionary.AccessControlExposeHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlExposeHeaders, value); } } @@ -1675,7 +1670,6 @@ StringValues IHeaderDictionary.AccessControlMaxAge set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AccessControlMaxAge, value); } } @@ -1693,7 +1687,6 @@ StringValues IHeaderDictionary.Age set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Age, value); } } @@ -1711,7 +1704,6 @@ StringValues IHeaderDictionary.Allow set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Allow, value); } } @@ -1729,7 +1721,6 @@ StringValues IHeaderDictionary.AltSvc set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.AltSvc, value); } } @@ -1747,7 +1738,6 @@ StringValues IHeaderDictionary.ContentDisposition set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentDisposition, value); } } @@ -1765,7 +1755,6 @@ StringValues IHeaderDictionary.ContentEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentEncoding, value); } } @@ -1783,7 +1772,6 @@ StringValues IHeaderDictionary.ContentLanguage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentLanguage, value); } } @@ -1801,7 +1789,6 @@ StringValues IHeaderDictionary.ContentLocation set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentLocation, value); } } @@ -1819,7 +1806,6 @@ StringValues IHeaderDictionary.ContentMD5 set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentMD5, value); } } @@ -1837,7 +1823,6 @@ StringValues IHeaderDictionary.ContentRange set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentRange, value); } } @@ -1855,7 +1840,6 @@ StringValues IHeaderDictionary.ContentSecurityPolicy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentSecurityPolicy, value); } } @@ -1873,7 +1857,6 @@ StringValues IHeaderDictionary.ContentSecurityPolicyReportOnly set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ContentSecurityPolicyReportOnly, value); } } @@ -1891,7 +1874,6 @@ StringValues IHeaderDictionary.ETag set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ETag, value); } } @@ -1909,7 +1891,6 @@ StringValues IHeaderDictionary.Expires set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Expires, value); } } @@ -1927,7 +1908,6 @@ StringValues IHeaderDictionary.GrpcMessage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.GrpcMessage, value); } } @@ -1945,7 +1925,6 @@ StringValues IHeaderDictionary.GrpcStatus set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.GrpcStatus, value); } } @@ -1963,7 +1942,6 @@ StringValues IHeaderDictionary.LastModified set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.LastModified, value); } } @@ -1981,7 +1959,6 @@ StringValues IHeaderDictionary.Link set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Link, value); } } @@ -1999,7 +1976,6 @@ StringValues IHeaderDictionary.Location set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Location, value); } } @@ -2017,7 +1993,6 @@ StringValues IHeaderDictionary.ProxyAuthenticate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ProxyAuthenticate, value); } } @@ -2035,7 +2010,6 @@ StringValues IHeaderDictionary.ProxyConnection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.ProxyConnection, value); } } @@ -2053,7 +2027,6 @@ StringValues IHeaderDictionary.RetryAfter set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.RetryAfter, value); } } @@ -2071,7 +2044,6 @@ StringValues IHeaderDictionary.SecWebSocketAccept set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SecWebSocketAccept, value); } } @@ -2089,7 +2061,6 @@ StringValues IHeaderDictionary.SecWebSocketKey set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SecWebSocketKey, value); } } @@ -2107,7 +2078,6 @@ StringValues IHeaderDictionary.SecWebSocketProtocol set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SecWebSocketProtocol, value); } } @@ -2125,7 +2095,6 @@ StringValues IHeaderDictionary.SecWebSocketVersion set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SecWebSocketVersion, value); } } @@ -2143,7 +2112,6 @@ StringValues IHeaderDictionary.SecWebSocketExtensions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SecWebSocketExtensions, value); } } @@ -2161,7 +2129,6 @@ StringValues IHeaderDictionary.Server set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Server, value); } } @@ -2179,7 +2146,6 @@ StringValues IHeaderDictionary.SetCookie set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.SetCookie, value); } } @@ -2197,7 +2163,6 @@ StringValues IHeaderDictionary.StrictTransportSecurity set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.StrictTransportSecurity, value); } } @@ -2215,7 +2180,6 @@ StringValues IHeaderDictionary.Trailer set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Trailer, value); } } @@ -2233,7 +2197,6 @@ StringValues IHeaderDictionary.Vary set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.Vary, value); } } @@ -2251,7 +2214,6 @@ StringValues IHeaderDictionary.WebSocketSubProtocols set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.WebSocketSubProtocols, value); } } @@ -2269,7 +2231,6 @@ StringValues IHeaderDictionary.WWWAuthenticate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.WWWAuthenticate, value); } } @@ -2287,7 +2248,6 @@ StringValues IHeaderDictionary.XContentTypeOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XContentTypeOptions, value); } } @@ -2305,7 +2265,6 @@ StringValues IHeaderDictionary.XFrameOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XFrameOptions, value); } } @@ -2323,7 +2282,6 @@ StringValues IHeaderDictionary.XPoweredBy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XPoweredBy, value); } } @@ -2341,7 +2299,6 @@ StringValues IHeaderDictionary.XRequestedWith set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XRequestedWith, value); } } @@ -2359,7 +2316,6 @@ StringValues IHeaderDictionary.XUACompatible set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XUACompatible, value); } } @@ -2377,7 +2333,6 @@ StringValues IHeaderDictionary.XXSSProtection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - SetValueUnknown(HeaderNames.XXSSProtection, value); } } @@ -7354,13 +7309,15 @@ public unsafe void Append(ReadOnlySpan name, ReadOnlySpan value) } else if (((Unsafe.ReadUnaligned(ref nameStart) & 0xffdfdfdfdfdfdfdfuL) == 0x2d544e45544e4f43uL) && ((Unsafe.ReadUnaligned(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x474e454cu) && ((Unsafe.ReadUnaligned(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(6 * sizeof(ushort)))) & 0xdfdfu) == 0x4854u)) { - if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + var customEncoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : EncodingSelector(HeaderNames.ContentLength); + if (customEncoding == null) { AppendContentLength(value); } else { - AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + AppendContentLengthCustomEncoding(value, customEncoding); } return; } @@ -7561,13 +7518,15 @@ public unsafe bool TryHPackAppend(int index, ReadOnlySpan value) nameStr = HeaderNames.CacheControl; break; case 28: - if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + var customEncoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : EncodingSelector(HeaderNames.ContentLength); + if (customEncoding == null) { AppendContentLength(value); } else { - AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + AppendContentLengthCustomEncoding(value, customEncoding); } return true; case 31: @@ -8333,6 +8292,7 @@ StringValues IHeaderDictionary.Connection var flag = 0x1L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Connection, value, EncodingSelector); _bits |= flag; _headers._Connection = value; } @@ -8362,6 +8322,7 @@ StringValues IHeaderDictionary.ContentType var flag = 0x2L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentType, value, EncodingSelector); _bits |= flag; _headers._ContentType = value; } @@ -8390,6 +8351,7 @@ StringValues IHeaderDictionary.Date var flag = 0x4L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Date, value, EncodingSelector); _bits |= flag; _headers._Date = value; } @@ -8419,6 +8381,7 @@ StringValues IHeaderDictionary.Server var flag = 0x8L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Server, value, EncodingSelector); _bits |= flag; _headers._Server = value; } @@ -8448,6 +8411,7 @@ StringValues IHeaderDictionary.AcceptRanges var flag = 0x10L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AcceptRanges, value, EncodingSelector); _bits |= flag; _headers._AcceptRanges = value; } @@ -8476,6 +8440,7 @@ StringValues IHeaderDictionary.AccessControlAllowCredentials var flag = 0x20L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowCredentials, value, EncodingSelector); _bits |= flag; _headers._AccessControlAllowCredentials = value; } @@ -8504,6 +8469,7 @@ StringValues IHeaderDictionary.AccessControlAllowHeaders var flag = 0x40L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowHeaders, value, EncodingSelector); _bits |= flag; _headers._AccessControlAllowHeaders = value; } @@ -8532,6 +8498,7 @@ StringValues IHeaderDictionary.AccessControlAllowMethods var flag = 0x80L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowMethods, value, EncodingSelector); _bits |= flag; _headers._AccessControlAllowMethods = value; } @@ -8560,6 +8527,7 @@ StringValues IHeaderDictionary.AccessControlAllowOrigin var flag = 0x100L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowOrigin, value, EncodingSelector); _bits |= flag; _headers._AccessControlAllowOrigin = value; } @@ -8588,6 +8556,7 @@ StringValues IHeaderDictionary.AccessControlExposeHeaders var flag = 0x200L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlExposeHeaders, value, EncodingSelector); _bits |= flag; _headers._AccessControlExposeHeaders = value; } @@ -8616,6 +8585,7 @@ StringValues IHeaderDictionary.AccessControlMaxAge var flag = 0x400L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AccessControlMaxAge, value, EncodingSelector); _bits |= flag; _headers._AccessControlMaxAge = value; } @@ -8644,6 +8614,7 @@ StringValues IHeaderDictionary.Age var flag = 0x800L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Age, value, EncodingSelector); _bits |= flag; _headers._Age = value; } @@ -8672,6 +8643,7 @@ StringValues IHeaderDictionary.Allow var flag = 0x1000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Allow, value, EncodingSelector); _bits |= flag; _headers._Allow = value; } @@ -8700,6 +8672,7 @@ StringValues IHeaderDictionary.AltSvc var flag = 0x2000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.AltSvc, value, EncodingSelector); _bits |= flag; _headers._AltSvc = value; } @@ -8728,6 +8701,7 @@ StringValues IHeaderDictionary.CacheControl var flag = 0x4000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.CacheControl, value, EncodingSelector); _bits |= flag; _headers._CacheControl = value; } @@ -8756,6 +8730,7 @@ StringValues IHeaderDictionary.ContentEncoding var flag = 0x8000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentEncoding, value, EncodingSelector); _bits |= flag; _headers._ContentEncoding = value; } @@ -8784,6 +8759,7 @@ StringValues IHeaderDictionary.ContentLanguage var flag = 0x10000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentLanguage, value, EncodingSelector); _bits |= flag; _headers._ContentLanguage = value; } @@ -8812,6 +8788,7 @@ StringValues IHeaderDictionary.ContentLocation var flag = 0x20000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentLocation, value, EncodingSelector); _bits |= flag; _headers._ContentLocation = value; } @@ -8840,6 +8817,7 @@ StringValues IHeaderDictionary.ContentMD5 var flag = 0x40000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentMD5, value, EncodingSelector); _bits |= flag; _headers._ContentMD5 = value; } @@ -8868,6 +8846,7 @@ StringValues IHeaderDictionary.ContentRange var flag = 0x80000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ContentRange, value, EncodingSelector); _bits |= flag; _headers._ContentRange = value; } @@ -8896,6 +8875,7 @@ StringValues IHeaderDictionary.ETag var flag = 0x100000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ETag, value, EncodingSelector); _bits |= flag; _headers._ETag = value; } @@ -8924,6 +8904,7 @@ StringValues IHeaderDictionary.Expires var flag = 0x200000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Expires, value, EncodingSelector); _bits |= flag; _headers._Expires = value; } @@ -8952,6 +8933,7 @@ StringValues IHeaderDictionary.GrpcEncoding var flag = 0x400000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.GrpcEncoding, value, EncodingSelector); _bits |= flag; _headers._GrpcEncoding = value; } @@ -8980,6 +8962,7 @@ StringValues IHeaderDictionary.KeepAlive var flag = 0x800000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.KeepAlive, value, EncodingSelector); _bits |= flag; _headers._KeepAlive = value; } @@ -9008,6 +8991,7 @@ StringValues IHeaderDictionary.LastModified var flag = 0x1000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.LastModified, value, EncodingSelector); _bits |= flag; _headers._LastModified = value; } @@ -9036,6 +9020,7 @@ StringValues IHeaderDictionary.Location var flag = 0x2000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Location, value, EncodingSelector); _bits |= flag; _headers._Location = value; } @@ -9064,6 +9049,7 @@ StringValues IHeaderDictionary.Pragma var flag = 0x4000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Pragma, value, EncodingSelector); _bits |= flag; _headers._Pragma = value; } @@ -9092,6 +9078,7 @@ StringValues IHeaderDictionary.ProxyAuthenticate var flag = 0x8000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ProxyAuthenticate, value, EncodingSelector); _bits |= flag; _headers._ProxyAuthenticate = value; } @@ -9120,6 +9107,7 @@ StringValues IHeaderDictionary.ProxyConnection var flag = 0x10000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ProxyConnection, value, EncodingSelector); _bits |= flag; _headers._ProxyConnection = value; } @@ -9148,6 +9136,7 @@ StringValues IHeaderDictionary.RetryAfter var flag = 0x20000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.RetryAfter, value, EncodingSelector); _bits |= flag; _headers._RetryAfter = value; } @@ -9176,6 +9165,7 @@ StringValues IHeaderDictionary.SetCookie var flag = 0x40000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.SetCookie, value, EncodingSelector); _bits |= flag; _headers._SetCookie = value; } @@ -9204,6 +9194,7 @@ StringValues IHeaderDictionary.Trailer var flag = 0x80000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Trailer, value, EncodingSelector); _bits |= flag; _headers._Trailer = value; } @@ -9232,6 +9223,7 @@ StringValues IHeaderDictionary.TransferEncoding var flag = 0x100000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.TransferEncoding, value, EncodingSelector); _bits |= flag; _headers._TransferEncoding = value; } @@ -9261,6 +9253,7 @@ StringValues IHeaderDictionary.Upgrade var flag = 0x200000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Upgrade, value, EncodingSelector); _bits |= flag; _headers._Upgrade = value; } @@ -9289,6 +9282,7 @@ StringValues IHeaderDictionary.Vary var flag = 0x400000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Vary, value, EncodingSelector); _bits |= flag; _headers._Vary = value; } @@ -9317,6 +9311,7 @@ StringValues IHeaderDictionary.Via var flag = 0x800000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Via, value, EncodingSelector); _bits |= flag; _headers._Via = value; } @@ -9345,6 +9340,7 @@ StringValues IHeaderDictionary.Warning var flag = 0x1000000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.Warning, value, EncodingSelector); _bits |= flag; _headers._Warning = value; } @@ -9373,6 +9369,7 @@ StringValues IHeaderDictionary.WWWAuthenticate var flag = 0x2000000000L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.WWWAuthenticate, value, EncodingSelector); _bits |= flag; _headers._WWWAuthenticate = value; } @@ -9398,7 +9395,7 @@ StringValues IHeaderDictionary.Accept set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Accept, value, EncodingSelector); SetValueUnknown(HeaderNames.Accept, value); } } @@ -9416,7 +9413,7 @@ StringValues IHeaderDictionary.AcceptCharset set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptCharset, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptCharset, value); } } @@ -9434,7 +9431,7 @@ StringValues IHeaderDictionary.AcceptEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptEncoding, value); } } @@ -9452,7 +9449,7 @@ StringValues IHeaderDictionary.AcceptLanguage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptLanguage, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptLanguage, value); } } @@ -9470,7 +9467,7 @@ StringValues IHeaderDictionary.AccessControlRequestHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlRequestHeaders, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlRequestHeaders, value); } } @@ -9488,7 +9485,7 @@ StringValues IHeaderDictionary.AccessControlRequestMethod set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlRequestMethod, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlRequestMethod, value); } } @@ -9506,7 +9503,7 @@ StringValues IHeaderDictionary.Authorization set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Authorization, value, EncodingSelector); SetValueUnknown(HeaderNames.Authorization, value); } } @@ -9524,7 +9521,7 @@ StringValues IHeaderDictionary.Baggage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Baggage, value, EncodingSelector); SetValueUnknown(HeaderNames.Baggage, value); } } @@ -9542,7 +9539,7 @@ StringValues IHeaderDictionary.ContentDisposition set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentDisposition, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentDisposition, value); } } @@ -9560,7 +9557,7 @@ StringValues IHeaderDictionary.ContentSecurityPolicy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentSecurityPolicy, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentSecurityPolicy, value); } } @@ -9578,7 +9575,7 @@ StringValues IHeaderDictionary.ContentSecurityPolicyReportOnly set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentSecurityPolicyReportOnly, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentSecurityPolicyReportOnly, value); } } @@ -9596,7 +9593,7 @@ StringValues IHeaderDictionary.CorrelationContext set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.CorrelationContext, value, EncodingSelector); SetValueUnknown(HeaderNames.CorrelationContext, value); } } @@ -9614,7 +9611,7 @@ StringValues IHeaderDictionary.Cookie set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Cookie, value, EncodingSelector); SetValueUnknown(HeaderNames.Cookie, value); } } @@ -9632,7 +9629,7 @@ StringValues IHeaderDictionary.Expect set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Expect, value, EncodingSelector); SetValueUnknown(HeaderNames.Expect, value); } } @@ -9650,7 +9647,7 @@ StringValues IHeaderDictionary.From set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.From, value, EncodingSelector); SetValueUnknown(HeaderNames.From, value); } } @@ -9668,7 +9665,7 @@ StringValues IHeaderDictionary.GrpcAcceptEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcAcceptEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcAcceptEncoding, value); } } @@ -9686,7 +9683,7 @@ StringValues IHeaderDictionary.GrpcMessage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcMessage, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcMessage, value); } } @@ -9704,7 +9701,7 @@ StringValues IHeaderDictionary.GrpcStatus set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcStatus, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcStatus, value); } } @@ -9722,7 +9719,7 @@ StringValues IHeaderDictionary.GrpcTimeout set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcTimeout, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcTimeout, value); } } @@ -9740,7 +9737,7 @@ StringValues IHeaderDictionary.Host set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Host, value, EncodingSelector); SetValueUnknown(HeaderNames.Host, value); } } @@ -9758,7 +9755,7 @@ StringValues IHeaderDictionary.IfMatch set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfMatch, value, EncodingSelector); SetValueUnknown(HeaderNames.IfMatch, value); } } @@ -9776,7 +9773,7 @@ StringValues IHeaderDictionary.IfModifiedSince set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfModifiedSince, value, EncodingSelector); SetValueUnknown(HeaderNames.IfModifiedSince, value); } } @@ -9794,7 +9791,7 @@ StringValues IHeaderDictionary.IfNoneMatch set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfNoneMatch, value, EncodingSelector); SetValueUnknown(HeaderNames.IfNoneMatch, value); } } @@ -9812,7 +9809,7 @@ StringValues IHeaderDictionary.IfRange set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfRange, value, EncodingSelector); SetValueUnknown(HeaderNames.IfRange, value); } } @@ -9830,7 +9827,7 @@ StringValues IHeaderDictionary.IfUnmodifiedSince set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfUnmodifiedSince, value, EncodingSelector); SetValueUnknown(HeaderNames.IfUnmodifiedSince, value); } } @@ -9848,7 +9845,7 @@ StringValues IHeaderDictionary.Link set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Link, value, EncodingSelector); SetValueUnknown(HeaderNames.Link, value); } } @@ -9866,7 +9863,7 @@ StringValues IHeaderDictionary.MaxForwards set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.MaxForwards, value, EncodingSelector); SetValueUnknown(HeaderNames.MaxForwards, value); } } @@ -9884,7 +9881,7 @@ StringValues IHeaderDictionary.Origin set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Origin, value, EncodingSelector); SetValueUnknown(HeaderNames.Origin, value); } } @@ -9902,7 +9899,7 @@ StringValues IHeaderDictionary.ProxyAuthorization set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ProxyAuthorization, value, EncodingSelector); SetValueUnknown(HeaderNames.ProxyAuthorization, value); } } @@ -9920,7 +9917,7 @@ StringValues IHeaderDictionary.Range set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Range, value, EncodingSelector); SetValueUnknown(HeaderNames.Range, value); } } @@ -9938,7 +9935,7 @@ StringValues IHeaderDictionary.Referer set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Referer, value, EncodingSelector); SetValueUnknown(HeaderNames.Referer, value); } } @@ -9956,7 +9953,7 @@ StringValues IHeaderDictionary.RequestId set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.RequestId, value, EncodingSelector); SetValueUnknown(HeaderNames.RequestId, value); } } @@ -9974,7 +9971,7 @@ StringValues IHeaderDictionary.SecWebSocketAccept set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketAccept, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketAccept, value); } } @@ -9992,7 +9989,7 @@ StringValues IHeaderDictionary.SecWebSocketKey set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketKey, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketKey, value); } } @@ -10010,7 +10007,7 @@ StringValues IHeaderDictionary.SecWebSocketProtocol set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketProtocol, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketProtocol, value); } } @@ -10028,7 +10025,7 @@ StringValues IHeaderDictionary.SecWebSocketVersion set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketVersion, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketVersion, value); } } @@ -10046,7 +10043,7 @@ StringValues IHeaderDictionary.SecWebSocketExtensions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketExtensions, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketExtensions, value); } } @@ -10064,7 +10061,7 @@ StringValues IHeaderDictionary.StrictTransportSecurity set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.StrictTransportSecurity, value, EncodingSelector); SetValueUnknown(HeaderNames.StrictTransportSecurity, value); } } @@ -10082,7 +10079,7 @@ StringValues IHeaderDictionary.TE set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TE, value, EncodingSelector); SetValueUnknown(HeaderNames.TE, value); } } @@ -10100,7 +10097,7 @@ StringValues IHeaderDictionary.Translate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Translate, value, EncodingSelector); SetValueUnknown(HeaderNames.Translate, value); } } @@ -10118,7 +10115,7 @@ StringValues IHeaderDictionary.TraceParent set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TraceParent, value, EncodingSelector); SetValueUnknown(HeaderNames.TraceParent, value); } } @@ -10136,7 +10133,7 @@ StringValues IHeaderDictionary.TraceState set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TraceState, value, EncodingSelector); SetValueUnknown(HeaderNames.TraceState, value); } } @@ -10154,7 +10151,7 @@ StringValues IHeaderDictionary.UpgradeInsecureRequests set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.UpgradeInsecureRequests, value, EncodingSelector); SetValueUnknown(HeaderNames.UpgradeInsecureRequests, value); } } @@ -10172,7 +10169,7 @@ StringValues IHeaderDictionary.UserAgent set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.UserAgent, value, EncodingSelector); SetValueUnknown(HeaderNames.UserAgent, value); } } @@ -10190,7 +10187,7 @@ StringValues IHeaderDictionary.WebSocketSubProtocols set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.WebSocketSubProtocols, value, EncodingSelector); SetValueUnknown(HeaderNames.WebSocketSubProtocols, value); } } @@ -10208,7 +10205,7 @@ StringValues IHeaderDictionary.XContentTypeOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XContentTypeOptions, value, EncodingSelector); SetValueUnknown(HeaderNames.XContentTypeOptions, value); } } @@ -10226,7 +10223,7 @@ StringValues IHeaderDictionary.XFrameOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XFrameOptions, value, EncodingSelector); SetValueUnknown(HeaderNames.XFrameOptions, value); } } @@ -10244,7 +10241,7 @@ StringValues IHeaderDictionary.XPoweredBy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XPoweredBy, value, EncodingSelector); SetValueUnknown(HeaderNames.XPoweredBy, value); } } @@ -10262,7 +10259,7 @@ StringValues IHeaderDictionary.XRequestedWith set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XRequestedWith, value, EncodingSelector); SetValueUnknown(HeaderNames.XRequestedWith, value); } } @@ -10280,7 +10277,7 @@ StringValues IHeaderDictionary.XUACompatible set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XUACompatible, value, EncodingSelector); SetValueUnknown(HeaderNames.XUACompatible, value); } } @@ -10298,7 +10295,7 @@ StringValues IHeaderDictionary.XXSSProtection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XXSSProtection, value, EncodingSelector); SetValueUnknown(HeaderNames.XXSSProtection, value); } } @@ -11141,7 +11138,7 @@ protected override bool TryGetValueFast(string key, out StringValues value) protected override void SetValueFast(string key, StringValues value) { - ValidateHeaderValueCharacters(value); + ValidateHeaderValueCharacters(key, value, EncodingSelector); switch (key.Length) { case 3: @@ -11720,7 +11717,7 @@ protected override void SetValueFast(string key, StringValues value) protected override bool AddValueFast(string key, StringValues value) { - ValidateHeaderValueCharacters(value); + ValidateHeaderValueCharacters(key, value, EncodingSelector); switch (key.Length) { case 3: @@ -14282,6 +14279,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) { int keyStart; int keyLength; + var headerName = string.Empty; switch (next) { case 0: // Header: "Connection" @@ -14299,6 +14297,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) values = ref _headers._Connection; keyStart = 0; keyLength = 14; + headerName = HeaderNames.Connection; } break; // OutputHeader @@ -14324,6 +14323,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) values = ref _headers._Date; keyStart = 30; keyLength = 8; + headerName = HeaderNames.Date; } break; // OutputHeader @@ -14342,6 +14342,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) values = ref _headers._Server; keyStart = 38; keyLength = 10; + headerName = HeaderNames.Server; } break; // OutputHeader @@ -14556,6 +14557,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) values = ref _headers._TransferEncoding; keyStart = 562; keyLength = 21; + headerName = HeaderNames.TransferEncoding; } break; // OutputHeader @@ -14603,6 +14605,8 @@ internal unsafe void CopyToFast(ref BufferWriter output) { // Clear bit tempBits ^= (1UL << next); + var encoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : EncodingSelector(headerName); var valueCount = values.Count; Debug.Assert(valueCount > 0); @@ -14613,7 +14617,14 @@ internal unsafe void CopyToFast(ref BufferWriter output) if (value != null) { output.Write(headerKey); - output.WriteAscii(value); + if (encoding is null) + { + output.WriteAscii(value); + } + else + { + output.WriteEncoded(value, encoding); + } } } // Set exact next @@ -15113,6 +15124,7 @@ StringValues IHeaderDictionary.ETag var flag = 0x1L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.ETag, value, EncodingSelector); _bits |= flag; _headers._ETag = value; } @@ -15141,6 +15153,7 @@ StringValues IHeaderDictionary.GrpcMessage var flag = 0x2L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.GrpcMessage, value, EncodingSelector); _bits |= flag; _headers._GrpcMessage = value; } @@ -15169,6 +15182,7 @@ StringValues IHeaderDictionary.GrpcStatus var flag = 0x4L; if (value.Count > 0) { + ValidateHeaderValueCharacters(HeaderNames.GrpcStatus, value, EncodingSelector); _bits |= flag; _headers._GrpcStatus = value; } @@ -15194,7 +15208,7 @@ StringValues IHeaderDictionary.Accept set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Accept, value, EncodingSelector); SetValueUnknown(HeaderNames.Accept, value); } } @@ -15212,7 +15226,7 @@ StringValues IHeaderDictionary.AcceptCharset set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptCharset, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptCharset, value); } } @@ -15230,7 +15244,7 @@ StringValues IHeaderDictionary.AcceptEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptEncoding, value); } } @@ -15248,7 +15262,7 @@ StringValues IHeaderDictionary.AcceptLanguage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptLanguage, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptLanguage, value); } } @@ -15266,7 +15280,7 @@ StringValues IHeaderDictionary.AcceptRanges set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AcceptRanges, value, EncodingSelector); SetValueUnknown(HeaderNames.AcceptRanges, value); } } @@ -15284,7 +15298,7 @@ StringValues IHeaderDictionary.AccessControlAllowCredentials set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowCredentials, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlAllowCredentials, value); } } @@ -15302,7 +15316,7 @@ StringValues IHeaderDictionary.AccessControlAllowHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowHeaders, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlAllowHeaders, value); } } @@ -15320,7 +15334,7 @@ StringValues IHeaderDictionary.AccessControlAllowMethods set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowMethods, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlAllowMethods, value); } } @@ -15338,7 +15352,7 @@ StringValues IHeaderDictionary.AccessControlAllowOrigin set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlAllowOrigin, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlAllowOrigin, value); } } @@ -15356,7 +15370,7 @@ StringValues IHeaderDictionary.AccessControlExposeHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlExposeHeaders, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlExposeHeaders, value); } } @@ -15374,7 +15388,7 @@ StringValues IHeaderDictionary.AccessControlMaxAge set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlMaxAge, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlMaxAge, value); } } @@ -15392,7 +15406,7 @@ StringValues IHeaderDictionary.AccessControlRequestHeaders set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlRequestHeaders, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlRequestHeaders, value); } } @@ -15410,7 +15424,7 @@ StringValues IHeaderDictionary.AccessControlRequestMethod set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AccessControlRequestMethod, value, EncodingSelector); SetValueUnknown(HeaderNames.AccessControlRequestMethod, value); } } @@ -15428,7 +15442,7 @@ StringValues IHeaderDictionary.Age set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Age, value, EncodingSelector); SetValueUnknown(HeaderNames.Age, value); } } @@ -15446,7 +15460,7 @@ StringValues IHeaderDictionary.Allow set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Allow, value, EncodingSelector); SetValueUnknown(HeaderNames.Allow, value); } } @@ -15464,7 +15478,7 @@ StringValues IHeaderDictionary.AltSvc set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.AltSvc, value, EncodingSelector); SetValueUnknown(HeaderNames.AltSvc, value); } } @@ -15482,7 +15496,7 @@ StringValues IHeaderDictionary.Authorization set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Authorization, value, EncodingSelector); SetValueUnknown(HeaderNames.Authorization, value); } } @@ -15500,7 +15514,7 @@ StringValues IHeaderDictionary.Baggage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Baggage, value, EncodingSelector); SetValueUnknown(HeaderNames.Baggage, value); } } @@ -15518,7 +15532,7 @@ StringValues IHeaderDictionary.CacheControl set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.CacheControl, value, EncodingSelector); SetValueUnknown(HeaderNames.CacheControl, value); } } @@ -15536,7 +15550,7 @@ StringValues IHeaderDictionary.Connection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Connection, value, EncodingSelector); SetValueUnknown(HeaderNames.Connection, value); } } @@ -15554,7 +15568,7 @@ StringValues IHeaderDictionary.ContentDisposition set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentDisposition, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentDisposition, value); } } @@ -15572,7 +15586,7 @@ StringValues IHeaderDictionary.ContentEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentEncoding, value); } } @@ -15590,7 +15604,7 @@ StringValues IHeaderDictionary.ContentLanguage set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentLanguage, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentLanguage, value); } } @@ -15608,7 +15622,7 @@ StringValues IHeaderDictionary.ContentLocation set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentLocation, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentLocation, value); } } @@ -15626,7 +15640,7 @@ StringValues IHeaderDictionary.ContentMD5 set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentMD5, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentMD5, value); } } @@ -15644,7 +15658,7 @@ StringValues IHeaderDictionary.ContentRange set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentRange, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentRange, value); } } @@ -15662,7 +15676,7 @@ StringValues IHeaderDictionary.ContentSecurityPolicy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentSecurityPolicy, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentSecurityPolicy, value); } } @@ -15680,7 +15694,7 @@ StringValues IHeaderDictionary.ContentSecurityPolicyReportOnly set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentSecurityPolicyReportOnly, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentSecurityPolicyReportOnly, value); } } @@ -15698,7 +15712,7 @@ StringValues IHeaderDictionary.ContentType set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ContentType, value, EncodingSelector); SetValueUnknown(HeaderNames.ContentType, value); } } @@ -15716,7 +15730,7 @@ StringValues IHeaderDictionary.CorrelationContext set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.CorrelationContext, value, EncodingSelector); SetValueUnknown(HeaderNames.CorrelationContext, value); } } @@ -15734,7 +15748,7 @@ StringValues IHeaderDictionary.Cookie set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Cookie, value, EncodingSelector); SetValueUnknown(HeaderNames.Cookie, value); } } @@ -15752,7 +15766,7 @@ StringValues IHeaderDictionary.Date set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Date, value, EncodingSelector); SetValueUnknown(HeaderNames.Date, value); } } @@ -15770,7 +15784,7 @@ StringValues IHeaderDictionary.Expires set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Expires, value, EncodingSelector); SetValueUnknown(HeaderNames.Expires, value); } } @@ -15788,7 +15802,7 @@ StringValues IHeaderDictionary.Expect set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Expect, value, EncodingSelector); SetValueUnknown(HeaderNames.Expect, value); } } @@ -15806,7 +15820,7 @@ StringValues IHeaderDictionary.From set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.From, value, EncodingSelector); SetValueUnknown(HeaderNames.From, value); } } @@ -15824,7 +15838,7 @@ StringValues IHeaderDictionary.GrpcAcceptEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcAcceptEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcAcceptEncoding, value); } } @@ -15842,7 +15856,7 @@ StringValues IHeaderDictionary.GrpcEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcEncoding, value); } } @@ -15860,7 +15874,7 @@ StringValues IHeaderDictionary.GrpcTimeout set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.GrpcTimeout, value, EncodingSelector); SetValueUnknown(HeaderNames.GrpcTimeout, value); } } @@ -15878,7 +15892,7 @@ StringValues IHeaderDictionary.Host set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Host, value, EncodingSelector); SetValueUnknown(HeaderNames.Host, value); } } @@ -15896,7 +15910,7 @@ StringValues IHeaderDictionary.KeepAlive set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.KeepAlive, value, EncodingSelector); SetValueUnknown(HeaderNames.KeepAlive, value); } } @@ -15914,7 +15928,7 @@ StringValues IHeaderDictionary.IfMatch set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfMatch, value, EncodingSelector); SetValueUnknown(HeaderNames.IfMatch, value); } } @@ -15932,7 +15946,7 @@ StringValues IHeaderDictionary.IfModifiedSince set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfModifiedSince, value, EncodingSelector); SetValueUnknown(HeaderNames.IfModifiedSince, value); } } @@ -15950,7 +15964,7 @@ StringValues IHeaderDictionary.IfNoneMatch set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfNoneMatch, value, EncodingSelector); SetValueUnknown(HeaderNames.IfNoneMatch, value); } } @@ -15968,7 +15982,7 @@ StringValues IHeaderDictionary.IfRange set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfRange, value, EncodingSelector); SetValueUnknown(HeaderNames.IfRange, value); } } @@ -15986,7 +16000,7 @@ StringValues IHeaderDictionary.IfUnmodifiedSince set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.IfUnmodifiedSince, value, EncodingSelector); SetValueUnknown(HeaderNames.IfUnmodifiedSince, value); } } @@ -16004,7 +16018,7 @@ StringValues IHeaderDictionary.LastModified set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.LastModified, value, EncodingSelector); SetValueUnknown(HeaderNames.LastModified, value); } } @@ -16022,7 +16036,7 @@ StringValues IHeaderDictionary.Link set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Link, value, EncodingSelector); SetValueUnknown(HeaderNames.Link, value); } } @@ -16040,7 +16054,7 @@ StringValues IHeaderDictionary.Location set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Location, value, EncodingSelector); SetValueUnknown(HeaderNames.Location, value); } } @@ -16058,7 +16072,7 @@ StringValues IHeaderDictionary.MaxForwards set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.MaxForwards, value, EncodingSelector); SetValueUnknown(HeaderNames.MaxForwards, value); } } @@ -16076,7 +16090,7 @@ StringValues IHeaderDictionary.Origin set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Origin, value, EncodingSelector); SetValueUnknown(HeaderNames.Origin, value); } } @@ -16094,7 +16108,7 @@ StringValues IHeaderDictionary.Pragma set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Pragma, value, EncodingSelector); SetValueUnknown(HeaderNames.Pragma, value); } } @@ -16112,7 +16126,7 @@ StringValues IHeaderDictionary.ProxyAuthenticate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ProxyAuthenticate, value, EncodingSelector); SetValueUnknown(HeaderNames.ProxyAuthenticate, value); } } @@ -16130,7 +16144,7 @@ StringValues IHeaderDictionary.ProxyAuthorization set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ProxyAuthorization, value, EncodingSelector); SetValueUnknown(HeaderNames.ProxyAuthorization, value); } } @@ -16148,7 +16162,7 @@ StringValues IHeaderDictionary.ProxyConnection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.ProxyConnection, value, EncodingSelector); SetValueUnknown(HeaderNames.ProxyConnection, value); } } @@ -16166,7 +16180,7 @@ StringValues IHeaderDictionary.Range set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Range, value, EncodingSelector); SetValueUnknown(HeaderNames.Range, value); } } @@ -16184,7 +16198,7 @@ StringValues IHeaderDictionary.Referer set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Referer, value, EncodingSelector); SetValueUnknown(HeaderNames.Referer, value); } } @@ -16202,7 +16216,7 @@ StringValues IHeaderDictionary.RetryAfter set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.RetryAfter, value, EncodingSelector); SetValueUnknown(HeaderNames.RetryAfter, value); } } @@ -16220,7 +16234,7 @@ StringValues IHeaderDictionary.RequestId set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.RequestId, value, EncodingSelector); SetValueUnknown(HeaderNames.RequestId, value); } } @@ -16238,7 +16252,7 @@ StringValues IHeaderDictionary.SecWebSocketAccept set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketAccept, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketAccept, value); } } @@ -16256,7 +16270,7 @@ StringValues IHeaderDictionary.SecWebSocketKey set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketKey, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketKey, value); } } @@ -16274,7 +16288,7 @@ StringValues IHeaderDictionary.SecWebSocketProtocol set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketProtocol, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketProtocol, value); } } @@ -16292,7 +16306,7 @@ StringValues IHeaderDictionary.SecWebSocketVersion set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketVersion, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketVersion, value); } } @@ -16310,7 +16324,7 @@ StringValues IHeaderDictionary.SecWebSocketExtensions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SecWebSocketExtensions, value, EncodingSelector); SetValueUnknown(HeaderNames.SecWebSocketExtensions, value); } } @@ -16328,7 +16342,7 @@ StringValues IHeaderDictionary.Server set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Server, value, EncodingSelector); SetValueUnknown(HeaderNames.Server, value); } } @@ -16346,7 +16360,7 @@ StringValues IHeaderDictionary.SetCookie set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.SetCookie, value, EncodingSelector); SetValueUnknown(HeaderNames.SetCookie, value); } } @@ -16364,7 +16378,7 @@ StringValues IHeaderDictionary.StrictTransportSecurity set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.StrictTransportSecurity, value, EncodingSelector); SetValueUnknown(HeaderNames.StrictTransportSecurity, value); } } @@ -16382,7 +16396,7 @@ StringValues IHeaderDictionary.TE set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TE, value, EncodingSelector); SetValueUnknown(HeaderNames.TE, value); } } @@ -16400,7 +16414,7 @@ StringValues IHeaderDictionary.Trailer set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Trailer, value, EncodingSelector); SetValueUnknown(HeaderNames.Trailer, value); } } @@ -16418,7 +16432,7 @@ StringValues IHeaderDictionary.TransferEncoding set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TransferEncoding, value, EncodingSelector); SetValueUnknown(HeaderNames.TransferEncoding, value); } } @@ -16436,7 +16450,7 @@ StringValues IHeaderDictionary.Translate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Translate, value, EncodingSelector); SetValueUnknown(HeaderNames.Translate, value); } } @@ -16454,7 +16468,7 @@ StringValues IHeaderDictionary.TraceParent set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TraceParent, value, EncodingSelector); SetValueUnknown(HeaderNames.TraceParent, value); } } @@ -16472,7 +16486,7 @@ StringValues IHeaderDictionary.TraceState set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.TraceState, value, EncodingSelector); SetValueUnknown(HeaderNames.TraceState, value); } } @@ -16490,7 +16504,7 @@ StringValues IHeaderDictionary.Upgrade set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Upgrade, value, EncodingSelector); SetValueUnknown(HeaderNames.Upgrade, value); } } @@ -16508,7 +16522,7 @@ StringValues IHeaderDictionary.UpgradeInsecureRequests set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.UpgradeInsecureRequests, value, EncodingSelector); SetValueUnknown(HeaderNames.UpgradeInsecureRequests, value); } } @@ -16526,7 +16540,7 @@ StringValues IHeaderDictionary.UserAgent set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.UserAgent, value, EncodingSelector); SetValueUnknown(HeaderNames.UserAgent, value); } } @@ -16544,7 +16558,7 @@ StringValues IHeaderDictionary.Vary set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Vary, value, EncodingSelector); SetValueUnknown(HeaderNames.Vary, value); } } @@ -16562,7 +16576,7 @@ StringValues IHeaderDictionary.Via set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Via, value, EncodingSelector); SetValueUnknown(HeaderNames.Via, value); } } @@ -16580,7 +16594,7 @@ StringValues IHeaderDictionary.Warning set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.Warning, value, EncodingSelector); SetValueUnknown(HeaderNames.Warning, value); } } @@ -16598,7 +16612,7 @@ StringValues IHeaderDictionary.WebSocketSubProtocols set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.WebSocketSubProtocols, value, EncodingSelector); SetValueUnknown(HeaderNames.WebSocketSubProtocols, value); } } @@ -16616,7 +16630,7 @@ StringValues IHeaderDictionary.WWWAuthenticate set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.WWWAuthenticate, value, EncodingSelector); SetValueUnknown(HeaderNames.WWWAuthenticate, value); } } @@ -16634,7 +16648,7 @@ StringValues IHeaderDictionary.XContentTypeOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XContentTypeOptions, value, EncodingSelector); SetValueUnknown(HeaderNames.XContentTypeOptions, value); } } @@ -16652,7 +16666,7 @@ StringValues IHeaderDictionary.XFrameOptions set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XFrameOptions, value, EncodingSelector); SetValueUnknown(HeaderNames.XFrameOptions, value); } } @@ -16670,7 +16684,7 @@ StringValues IHeaderDictionary.XPoweredBy set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XPoweredBy, value, EncodingSelector); SetValueUnknown(HeaderNames.XPoweredBy, value); } } @@ -16688,7 +16702,7 @@ StringValues IHeaderDictionary.XRequestedWith set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XRequestedWith, value, EncodingSelector); SetValueUnknown(HeaderNames.XRequestedWith, value); } } @@ -16706,7 +16720,7 @@ StringValues IHeaderDictionary.XUACompatible set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XUACompatible, value, EncodingSelector); SetValueUnknown(HeaderNames.XUACompatible, value); } } @@ -16724,7 +16738,7 @@ StringValues IHeaderDictionary.XXSSProtection set { if (_isReadOnly) { ThrowHeadersReadOnlyException(); } - + ValidateHeaderValueCharacters(HeaderNames.XXSSProtection, value, EncodingSelector); SetValueUnknown(HeaderNames.XXSSProtection, value); } } @@ -16815,7 +16829,7 @@ protected override bool TryGetValueFast(string key, out StringValues value) protected override void SetValueFast(string key, StringValues value) { - ValidateHeaderValueCharacters(value); + ValidateHeaderValueCharacters(key, value, EncodingSelector); switch (key.Length) { case 4: @@ -16876,7 +16890,7 @@ protected override void SetValueFast(string key, StringValues value) protected override bool AddValueFast(string key, StringValues value) { - ValidateHeaderValueCharacters(value); + ValidateHeaderValueCharacters(key, value, EncodingSelector); switch (key.Length) { case 4: diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs index cd1b711d63a6..e27b38651920 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; @@ -260,21 +261,25 @@ bool IDictionary.TryGetValue(string key, out StringValues return TryGetValueFast(key, out value); } - public static void ValidateHeaderValueCharacters(StringValues headerValues) + public static void ValidateHeaderValueCharacters(string headerName, StringValues headerValues, Func encodingSelector) { + var requireAscii = ReferenceEquals(encodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + || encodingSelector(headerName) == null; + var count = headerValues.Count; for (var i = 0; i < count; i++) { - ValidateHeaderValueCharacters(headerValues[i]); + ValidateHeaderValueCharacters(headerValues[i], requireAscii); } } - public static void ValidateHeaderValueCharacters(string headerCharacters) + public static void ValidateHeaderValueCharacters(string headerCharacters, bool requireAscii) { if (headerCharacters != null) { - var invalid = HttpCharacters.IndexOfInvalidFieldValueChar(headerCharacters); + var invalid = requireAscii ? HttpCharacters.IndexOfInvalidFieldValueChar(headerCharacters) + : HttpCharacters.IndexOfInvalidFieldValueCharExtended(headerCharacters); if (invalid >= 0) { ThrowInvalidHeaderCharacter(headerCharacters[invalid]); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 99be0b19494a..d163b737b1cb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -374,6 +374,7 @@ public void Reset() HttpRequestHeaders.EncodingSelector = ServerOptions.RequestHeaderEncodingSelector; HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse; HttpResponseHeaders.Reset(); + HttpResponseHeaders.EncodingSelector = ServerOptions.ResponseHeaderEncodingSelector; RequestHeaders = HttpRequestHeaders; ResponseHeaders = HttpResponseHeaders; RequestTrailers.Clear(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index b9289e36807b..bf159d0f7b04 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -24,7 +24,7 @@ internal sealed partial class HttpRequestHeaders : HttpHeaders public HttpRequestHeaders(bool reuseHeaderValues = true, Func? encodingSelector = null) { ReuseHeaderValues = reuseHeaderValues; - EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultRequestHeaderEncodingSelector; + EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultHeaderEncodingSelector; } public void OnHeadersComplete() @@ -97,7 +97,7 @@ private void AppendContentLength(ReadOnlySpan value) [MethodImpl(MethodImplOptions.NoInlining)] [SkipLocalsInit] - private void AppendContentLengthCustomEncoding(ReadOnlySpan value, Encoding? customEncoding) + private void AppendContentLengthCustomEncoding(ReadOnlySpan value, Encoding customEncoding) { if (_contentLength.HasValue) { @@ -106,7 +106,7 @@ private void AppendContentLengthCustomEncoding(ReadOnlySpan value, Encodin // long.MaxValue = 9223372036854775807 (19 chars) Span decodedChars = stackalloc char[20]; - var numChars = customEncoding!.GetChars(value, decodedChars); + var numChars = customEncoding.GetChars(value, decodedChars); long parsed = -1; if (numChars > 19 || diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs index ae51e3c535e9..0e7b2e70f8c2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs @@ -4,10 +4,11 @@ using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.IO.Pipelines; using System.Collections; using System.Collections.Generic; +using System.IO.Pipelines; using System.Runtime.CompilerServices; +using System.Text; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -19,6 +20,13 @@ internal sealed partial class HttpResponseHeaders : HttpHeaders private static ReadOnlySpan CrLf => new[] { (byte)'\r', (byte)'\n' }; private static ReadOnlySpan ColonSpace => new[] { (byte)':', (byte)' ' }; + public Func EncodingSelector { get; set; } + + public HttpResponseHeaders(Func? encodingSelector = null) + { + EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultHeaderEncodingSelector; + } + public Enumerator GetEnumerator() { return new Enumerator(this); @@ -34,10 +42,18 @@ internal void CopyTo(ref BufferWriter buffer) CopyToFast(ref buffer); var extraHeaders = MaybeUnknown; + // Only reserve stack space for the enumerators if there are extra headers if (extraHeaders != null && extraHeaders.Count > 0) { - // Only reserve stack space for the enumartors if there are extra headers - CopyExtraHeaders(ref buffer, extraHeaders); + var encodingSelector = EncodingSelector; + if (ReferenceEquals(encodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)) + { + CopyExtraHeaders(ref buffer, extraHeaders); + } + else + { + CopyExtraHeadersCustomEncoding(ref buffer, extraHeaders, encodingSelector); + } } static void CopyExtraHeaders(ref BufferWriter buffer, Dictionary headers) @@ -56,6 +72,33 @@ static void CopyExtraHeaders(ref BufferWriter buffer, Dictionary buffer, Dictionary headers, + Func encodingSelector) + { + foreach (var kv in headers) + { + var encoding = encodingSelector(kv.Key); + foreach (var value in kv.Value) + { + if (value != null) + { + buffer.Write(CrLf); + buffer.WriteAscii(kv.Key); + buffer.Write(ColonSpace); + + if (encoding is null) + { + buffer.WriteAscii(value); + } + else + { + buffer.WriteEncoded(value, encoding); + } + } + } + } + } } private static long ParseContentLength(string value) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs index 3fb3cef5e385..4bb97f7e11b5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs @@ -5,12 +5,20 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Text; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal partial class HttpResponseTrailers : HttpHeaders { + public Func EncodingSelector { get; set; } + + public HttpResponseTrailers(Func? encodingSelector = null) + { + EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultHeaderEncodingSelector; + } + public Enumerator GetEnumerator() { return new Enumerator(this); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs index c57f1e28f518..1267e26326ab 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -87,7 +87,7 @@ private static bool EncodeStatusHeader(int statusCode, DynamicHPackEncoder hpack default: const string name = ":status"; var value = StatusCodes.ToStatusString(statusCode); - return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, out length); + return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, valueEncoding: null, out length); } } @@ -99,6 +99,9 @@ private static bool EncodeHeadersCore(DynamicHPackEncoder hpackEncoder, Http2Hea var staticTableId = headersEnumerator.HPackStaticTableId; var name = headersEnumerator.Current.Key; var value = headersEnumerator.Current.Value; + var valueEncoding = + ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : headersEnumerator.EncodingSelector(name); var hint = ResolveHeaderEncodingHint(staticTableId, name); @@ -108,6 +111,7 @@ private static bool EncodeHeadersCore(DynamicHPackEncoder hpackEncoder, Http2Hea hint, name, value, + valueEncoding, out var headerLength)) { // If the header wasn't written, and no headers have been written, then the header is too large. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 89d23dd5edf5..bb5baf254e96 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -189,11 +189,13 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } - catch (HPackEncodingException hex) + // Any exception from the HPack encoder can leave the dynamic table in a corrupt state. + // Since we allow custom header encoders we don't know what type of exceptions to expect. + catch (Exception ex) { - _log.HPackEncodingError(_connectionId, streamId, hex); - _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex)); - throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write. + _log.HPackEncodingError(_connectionId, streamId, ex); + _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex)); + throw new InvalidOperationException(ex.Message, ex); // Report the error to the user if this was the first write. } } } @@ -215,10 +217,12 @@ public ValueTask WriteResponseTrailersAsync(int streamId, HttpRespo var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } - catch (HPackEncodingException hex) + // Any exception from the HPack encoder can leave the dynamic table in a corrupt state. + // Since we allow custom header encoders we don't know what type of exceptions to expect. + catch (Exception ex) { - _log.HPackEncodingError(_connectionId, streamId, hex); - _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex)); + _log.HPackEncodingError(_connectionId, streamId, ex); + _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex)); } return TimeFlushUnsynchronizedAsync(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeadersEnumerator.cs similarity index 96% rename from src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs rename to src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeadersEnumerator.cs index c77836ab4c41..19b7399f28ff 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeadersEnumerator.cs @@ -1,9 +1,11 @@ // 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 System; using System.Collections; using System.Collections.Generic; using System.Net.Http.HPack; +using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.Extensions.Primitives; @@ -25,16 +27,15 @@ private enum HeadersType : byte private bool _hasMultipleValues; private KnownHeaderType _knownHeaderType; + public Func EncodingSelector { get; set; } = KestrelServerOptions.DefaultHeaderEncodingSelector; + public int HPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType); public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; - public Http2HeadersEnumerator() - { - } - public void Initialize(HttpResponseHeaders headers) { + EncodingSelector = headers.EncodingSelector; _headersEnumerator = headers.GetEnumerator(); _headersType = HeadersType.Headers; _hasMultipleValues = false; @@ -42,6 +43,7 @@ public void Initialize(HttpResponseHeaders headers) public void Initialize(HttpResponseTrailers headers) { + EncodingSelector = headers.EncodingSelector; _trailersEnumerator = headers.GetEnumerator(); _headersType = HeadersType.Trailers; _hasMultipleValues = false; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs index c848c9e46d5e..c38b525462f5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs @@ -25,7 +25,7 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers { if (ResponseTrailers == null) { - ResponseTrailers = new HttpResponseTrailers(); + ResponseTrailers = new HttpResponseTrailers(ServerOptions.ResponseHeaderEncodingSelector); if (HasResponseCompleted) { ResponseTrailers.SetReadOnly(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs index a7f40e0fbe1e..1584d9af16d4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -8,6 +8,7 @@ using System.IO.Pipelines; using System.Net.Http; using System.Net.Http.QPack; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -44,7 +45,7 @@ internal class Http3FrameWriter // Write headers to a buffer that can grow. Possible performance improvement // by writing directly to output writer (difficult as frame length is prefixed). private readonly ArrayBufferWriter _headerEncodingBuffer; - private IEnumerator>? _headersEnumerator; + private Http3HeadersEnumerator _headersEnumerator = new(); private int _headersTotalSize; private long _unflushedBytes; @@ -271,7 +272,7 @@ public ValueTask WriteResponseTrailersAsync(long streamId, HttpResp try { - _headersEnumerator = EnumerateHeaders(headers).GetEnumerator(); + _headersEnumerator.Initialize(headers); _headersTotalSize = 0; _headerEncodingBuffer.Clear(); @@ -280,9 +281,12 @@ public ValueTask WriteResponseTrailersAsync(long streamId, HttpResp var done = QPackHeaderWriter.BeginEncode(_headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength); FinishWritingHeaders(payloadLength, done); } - catch (QPackEncodingException ex) + // Any exception from the QPack encoder can leave the dynamic table in a corrupt state. + // Since we allow custom header encoders we don't know what type of exceptions to expect. + catch (Exception ex) { _log.QPackEncodingError(_connectionId, streamId, ex); + _connectionContext.Abort(new ConnectionAbortedException(ex.Message, ex)); _http3Stream.Abort(new ConnectionAbortedException(ex.Message, ex), Http3ErrorCode.InternalError); } @@ -314,7 +318,7 @@ public ValueTask FlushAsync(IHttpOutputAborter? outputAborter, Canc } } - internal void WriteResponseHeaders(int statusCode, IHeaderDictionary headers) + internal void WriteResponseHeaders(int statusCode, HttpResponseHeaders headers) { lock (_writeLock) { @@ -325,15 +329,19 @@ internal void WriteResponseHeaders(int statusCode, IHeaderDictionary headers) try { - _headersEnumerator = EnumerateHeaders(headers).GetEnumerator(); + _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(); var buffer = _headerEncodingBuffer.GetSpan(HeaderBufferSize); var done = QPackHeaderWriter.BeginEncode(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength); FinishWritingHeaders(payloadLength, done); } - catch (QPackEncodingException ex) + // Any exception from the QPack encoder can leave the dynamic table in a corrupt state. + // Since we allow custom header encoders we don't know what type of exceptions to expect. + catch (Exception ex) { + _log.QPackEncodingError(_connectionId, _http3Stream.StreamId, ex); + _connectionContext.Abort(new ConnectionAbortedException(ex.Message, ex)); _http3Stream.Abort(new ConnectionAbortedException(ex.Message, ex), Http3ErrorCode.InternalError); throw new InvalidOperationException(ex.Message, ex); // Report the error to the user if this was the first write. } @@ -347,7 +355,6 @@ private void FinishWritingHeaders(int payloadLength, bool done) while (!done) { ValidateHeadersTotalSize(); - var buffer = _headerEncodingBuffer.GetSpan(HeaderBufferSize); done = QPackHeaderWriter.Encode(_headersEnumerator!, buffer, ref _headersTotalSize, out payloadLength); _headerEncodingBuffer.Advance(payloadLength); @@ -404,16 +411,5 @@ public void Abort(ConnectionAbortedException error) _outputWriter.Complete(); } } - - private static IEnumerable> EnumerateHeaders(IHeaderDictionary headers) - { - foreach (var header in headers) - { - foreach (var value in header.Value) - { - yield return new KeyValuePair(header.Key, value); - } - } - } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs new file mode 100644 index 000000000000..67797a000f45 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs @@ -0,0 +1,152 @@ +// 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 System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal sealed class Http3HeadersEnumerator : IEnumerator> + { + private enum HeadersType : byte + { + Headers, + Trailers, + Untyped + } + private HeadersType _headersType; + private HttpResponseHeaders.Enumerator _headersEnumerator; + private HttpResponseTrailers.Enumerator _trailersEnumerator; + private IEnumerator>? _genericEnumerator; + private StringValues.Enumerator _stringValuesEnumerator; + private bool _hasMultipleValues; + private KnownHeaderType _knownHeaderType; + + public Func EncodingSelector { get; set; } = KestrelServerOptions.DefaultHeaderEncodingSelector; + + public int QPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType); + public KeyValuePair Current { get; private set; } + object IEnumerator.Current => Current; + + public void Initialize(HttpResponseHeaders headers) + { + EncodingSelector = headers.EncodingSelector; + _headersEnumerator = headers.GetEnumerator(); + _headersType = HeadersType.Headers; + _hasMultipleValues = false; + } + + public void Initialize(HttpResponseTrailers headers) + { + EncodingSelector = headers.EncodingSelector; + _trailersEnumerator = headers.GetEnumerator(); + _headersType = HeadersType.Trailers; + _hasMultipleValues = false; + } + + public void Initialize(IDictionary headers) + { + switch (headers) + { + case HttpResponseHeaders responseHeaders: + _headersType = HeadersType.Headers; + _headersEnumerator = responseHeaders.GetEnumerator(); + break; + case HttpResponseTrailers responseTrailers: + _headersType = HeadersType.Trailers; + _trailersEnumerator = responseTrailers.GetEnumerator(); + break; + default: + _headersType = HeadersType.Untyped; + _genericEnumerator = headers.GetEnumerator(); + break; + } + + _hasMultipleValues = false; + } + + public bool MoveNext() + { + if (_hasMultipleValues && MoveNextOnStringEnumerator(Current.Key)) + { + return true; + } + + if (_headersType == HeadersType.Headers) + { + return _headersEnumerator.MoveNext() + ? SetCurrent(_headersEnumerator.Current.Key, _headersEnumerator.Current.Value, _headersEnumerator.CurrentKnownType) + : false; + } + else if (_headersType == HeadersType.Trailers) + { + return _trailersEnumerator.MoveNext() + ? SetCurrent(_trailersEnumerator.Current.Key, _trailersEnumerator.Current.Value, _trailersEnumerator.CurrentKnownType) + : false; + } + else + { + return _genericEnumerator!.MoveNext() + ? SetCurrent(_genericEnumerator.Current.Key, _genericEnumerator.Current.Value, default) + : false; + } + } + + private bool MoveNextOnStringEnumerator(string key) + { + var result = _stringValuesEnumerator.MoveNext(); + Current = result ? new KeyValuePair(key, _stringValuesEnumerator.Current) : default; + return result; + } + + private bool SetCurrent(string name, StringValues value, KnownHeaderType knownHeaderType) + { + _knownHeaderType = knownHeaderType; + + if (value.Count == 1) + { + Current = new KeyValuePair(name, value.ToString()); + _hasMultipleValues = false; + return true; + } + else + { + _stringValuesEnumerator = value.GetEnumerator(); + _hasMultipleValues = true; + return MoveNextOnStringEnumerator(name); + } + } + + public void Reset() + { + if (_headersType == HeadersType.Headers) + { + _headersEnumerator.Reset(); + } + else if (_headersType == HeadersType.Trailers) + { + _trailersEnumerator.Reset(); + } + else + { + _genericEnumerator!.Reset(); + } + _stringValuesEnumerator = default; + _knownHeaderType = default; + } + + public void Dispose() + { + } + + internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) + { + // Not Implemented + return -1; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs index 90cc3b151e43..9aa6e85431f7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs @@ -23,7 +23,7 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers { if (ResponseTrailers == null) { - ResponseTrailers = new HttpResponseTrailers(); + ResponseTrailers = new HttpResponseTrailers(ServerOptions.ResponseHeaderEncodingSelector); if (HasResponseCompleted) { ResponseTrailers.SetReadOnly(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs index 3bb0ea80ced4..710adc1bb0e5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs @@ -2,7 +2,6 @@ // 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.Diagnostics; using System.Net.Http.QPack; @@ -10,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { internal static class QPackHeaderWriter { - public static bool BeginEncode(IEnumerator> enumerator, Span buffer, ref int totalHeaderSize, out int length) + public static bool BeginEncode(Http3HeadersEnumerator enumerator, Span buffer, ref int totalHeaderSize, out int length) { bool hasValue = enumerator.MoveNext(); Debug.Assert(hasValue == true); @@ -25,7 +24,7 @@ public static bool BeginEncode(IEnumerator> enumera return doneEncode; } - public static bool BeginEncode(int statusCode, IEnumerator> enumerator, Span buffer, ref int totalHeaderSize, out int length) + public static bool BeginEncode(int statusCode, Http3HeadersEnumerator enumerator, Span buffer, ref int totalHeaderSize, out int length) { bool hasValue = enumerator.MoveNext(); Debug.Assert(hasValue == true); @@ -43,20 +42,22 @@ public static bool BeginEncode(int statusCode, IEnumerator> enumerator, Span buffer, ref int totalHeaderSize, out int length) + public static bool Encode(Http3HeadersEnumerator enumerator, Span buffer, ref int totalHeaderSize, out int length) { return Encode(enumerator, buffer, throwIfNoneEncoded: true, ref totalHeaderSize, out length); } - private static bool Encode(IEnumerator> enumerator, Span buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length) + private static bool Encode(Http3HeadersEnumerator enumerator, Span buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length) { length = 0; do { var current = enumerator.Current; + var valueEncoding = ReferenceEquals(enumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : enumerator.EncodingSelector(current.Key); - if (!QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(current.Key, current.Value, buffer.Slice(length), out int headerLength)) + if (!QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(current.Key, current.Value, valueEncoding, buffer.Slice(length), out int headerLength)) { if (length == 0 && throwIfNoneEncoded) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs index d59ab76a2a41..a2faf32bb346 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -182,6 +182,7 @@ public static int IndexOfInvalidTokenChar(ReadOnlySpan span) return -1; } + // Disallows control characters and anything more than 0x7F [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOfInvalidFieldValueChar(string s) { @@ -198,5 +199,23 @@ public static int IndexOfInvalidFieldValueChar(string s) return -1; } + + // Disallows control characters but allows extended characters > 0x7F + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfInvalidFieldValueCharExtended(string s) + { + var fieldValue = _fieldValue; + + for (var i = 0; i < s.Length; i++) + { + var c = s[i]; + if (c < (uint)fieldValue.Length && !fieldValue[c]) + { + return i; + } + } + + return -1; + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index e97a6f6e9bba..f6324a7edec6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -110,7 +110,7 @@ public static string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan span, string name, Func encodingSelector) { - if (ReferenceEquals(KestrelServerOptions.DefaultRequestHeaderEncodingSelector, encodingSelector)) + if (ReferenceEquals(KestrelServerOptions.DefaultHeaderEncodingSelector, encodingSelector)) { return span.GetAsciiOrUTF8StringNonNullCharacters(DefaultRequestHeaderEncoding); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs index 8ec96189092d..c2248f14bee0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs @@ -70,9 +70,9 @@ internal interface IKestrelTrace : ILogger void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason); - void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex); + void HPackDecodingError(string connectionId, int streamId, Exception ex); - void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex); + void HPackEncodingError(string connectionId, int streamId, Exception ex); void Http2FrameReceived(string connectionId, Http2Frame frame); @@ -94,9 +94,9 @@ internal interface IKestrelTrace : ILogger void Http3FrameSending(string connectionId, long streamId, Http3RawFrame frame); - void QPackDecodingError(string connectionId, long streamId, QPackDecodingException ex); + void QPackDecodingError(string connectionId, long streamId, Exception ex); - void QPackEncodingError(string connectionId, long streamId, QPackEncodingException ex); + void QPackEncodingError(string connectionId, long streamId, Exception ex); void Http3OutboundControlStreamError(string connectionId, Exception ex); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs index 18e5b6392567..5f000f5b286a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs @@ -320,12 +320,12 @@ public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, _http2StreamResetAbort(_http2Logger, traceIdentifier, error, abortReason); } - public virtual void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) + public virtual void HPackDecodingError(string connectionId, int streamId, Exception ex) { _hpackDecodingError(_http2Logger, connectionId, streamId, ex); } - public virtual void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex) + public virtual void HPackEncodingError(string connectionId, int streamId, Exception ex) { _hpackEncodingError(_http2Logger, connectionId, streamId, ex); } @@ -395,12 +395,12 @@ public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame } } - public virtual void QPackDecodingError(string connectionId, long streamId, QPackDecodingException ex) + public virtual void QPackDecodingError(string connectionId, long streamId, Exception ex) { _qpackDecodingError(_http3Logger, connectionId, streamId, ex); } - public virtual void QPackEncodingError(string connectionId, long streamId, QPackEncodingException ex) + public virtual void QPackEncodingError(string connectionId, long streamId, Exception ex) { _qpackEncodingError(_http3Logger, connectionId, streamId, ex); } diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index d48c13b02188..a70a63810e4d 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -28,9 +28,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public class KestrelServerOptions { // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged. - internal static readonly Func DefaultRequestHeaderEncodingSelector = _ => null; + internal static readonly Func DefaultHeaderEncodingSelector = _ => null; - private Func _requestHeaderEncodingSelector = DefaultRequestHeaderEncodingSelector; + private Func _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector; + + private Func _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; // The following two lists configure the endpoints that Kestrel should listen to. If both lists are empty, the "urls" config setting (e.g. UseUrls) is used. internal List CodeBackedListenOptions { get; } = new List(); @@ -93,6 +95,16 @@ public class KestrelServerOptions set => _requestHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value)); } + ///

    + /// Gets or sets a callback that returns the to encode the value for the specified response header + /// or trailer name, or to use the default . + /// + public Func ResponseHeaderEncodingSelector + { + get => _responseHeaderEncodingSelector; + set => _responseHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value)); + } + /// /// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Typically initialized by UseKestrel(). diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index 8495f35e086c..a3e94fa64f7b 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -96,6 +96,8 @@ *REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password, System.Action configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions *REMOVED*~static Microsoft.AspNetCore.Server.Kestrel.Https.CertificateLoader.LoadFromStoreCert(string subject, string storeName, System.Security.Cryptography.X509Certificates.StoreLocation storeLocation, bool allowInvalid) -> System.Security.Cryptography.X509Certificates.X509Certificate2 Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.get -> System.Func! +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.set -> void static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Action! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs index 50c2411818c6..bca46ecb5afc 100644 --- a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs @@ -98,23 +98,29 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() { Assert.Equal("Location", e.Name); Assert.Equal("https://www.example.com", e.Value); + Assert.Equal(63u, e.Size); }, e => { Assert.Equal("Cache-Control", e.Name); Assert.Equal("private", e.Value); + Assert.Equal(52u, e.Size); }, e => { Assert.Equal("Date", e.Name); Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + Assert.Equal(65u, e.Size); }, e => { Assert.Equal(":status", e.Name); Assert.Equal("302", e.Value); + Assert.Equal(42u, e.Size); }); + Assert.Equal(222u, hpackEncoder.TableSize); + // Second response enumerator.Initialize(headers); Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length)); @@ -129,23 +135,29 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() { Assert.Equal(":status", e.Name); Assert.Equal("307", e.Value); + Assert.Equal(42u, e.Size); }, e => { Assert.Equal("Location", e.Name); Assert.Equal("https://www.example.com", e.Value); + Assert.Equal(63u, e.Size); }, e => { Assert.Equal("Cache-Control", e.Name); Assert.Equal("private", e.Value); + Assert.Equal(52u, e.Size); }, e => { Assert.Equal("Date", e.Name); Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + Assert.Equal(65u, e.Size); }); + Assert.Equal(222u, hpackEncoder.TableSize); + // Third response headers.Date = "Mon, 21 Oct 2013 20:13:22 GMT"; headers.ContentEncoding = "gzip"; @@ -171,22 +183,172 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() { Assert.Equal("Content-Encoding", e.Name); Assert.Equal("gzip", e.Value); + Assert.Equal(52u, e.Size); }, e => { Assert.Equal("Date", e.Name); Assert.Equal("Mon, 21 Oct 2013 20:13:22 GMT", e.Value); + Assert.Equal(65u, e.Size); }, e => { Assert.Equal(":status", e.Name); Assert.Equal("307", e.Value); + Assert.Equal(42u, e.Size); }, e => { Assert.Equal("Location", e.Name); Assert.Equal("https://www.example.com", e.Value); + Assert.Equal(63u, e.Size); + }); + + Assert.Equal(222u, hpackEncoder.TableSize); + } + + [Fact] + public void BeginEncodeHeadersCustomEncoding_MaxHeaderTableSizeExceeded_EvictionsToFit() + { + // Test follows example https://tools.ietf.org/html/rfc7541#appendix-C.5 + + Span buffer = new byte[1024 * 16]; + + var headers = (IHeaderDictionary)new HttpResponseHeaders(_ => Encoding.UTF8); + headers.CacheControl = "你好e"; + headers.Date = "Mon, 21 Oct 2013 20:13:21 GMT"; + headers.Location = "你好你好你好你.c"; + + var enumerator = new Http2HeadersEnumerator(); + + var hpackEncoder = new DynamicHPackEncoder(maxHeaderTableSize: 256); + + // First response + enumerator.Initialize((HttpResponseHeaders)headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); + + var result = buffer.Slice(0, length).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal( + "48-03-33-30-32-61-1D-4D-6F-6E-2C-20-32-31-20-4F-" + + "63-74-20-32-30-31-33-20-32-30-3A-31-33-3A-32-31-" + + "20-47-4D-54-58-07-E4-BD-A0-E5-A5-BD-65-6E-17-E4-" + + "BD-A0-E5-A5-BD-E4-BD-A0-E5-A5-BD-E4-BD-A0-E5-A5-" + + "BD-E4-BD-A0-2E-63", hex); + + var entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("你好你好你好你.c", e.Value); + Assert.Equal(63u, e.Size); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("你好e", e.Value); + Assert.Equal(52u, e.Size); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + Assert.Equal(65u, e.Size); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("302", e.Value); + Assert.Equal(42u, e.Size); }); + + Assert.Equal(222u, hpackEncoder.TableSize); + + // Second response + enumerator.Initialize(headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal("48-03-33-30-37-C1-C0-BF", hex); + + entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + Assert.Equal(42u, e.Size); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("你好你好你好你.c", e.Value); + Assert.Equal(63u, e.Size); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("你好e", e.Value); + Assert.Equal(52u, e.Size); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + Assert.Equal(65u, e.Size); + }); + + Assert.Equal(222u, hpackEncoder.TableSize); + + // Third response + headers.Date = "Mon, 21 Oct 2013 20:13:22 GMT"; + headers.ContentEncoding = "gzip"; + headers.SetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"; + + enumerator.Initialize(headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal( + "88-61-1D-4D-6F-6E-2C-20-32-31-20-4F-63-74-20-32-" + + "30-31-33-20-32-30-3A-31-33-3A-32-32-20-47-4D-54-" + + "C1-5A-04-67-7A-69-70-C1-1F-28-38-66-6F-6F-3D-41-" + + "53-44-4A-4B-48-51-4B-42-5A-58-4F-51-57-45-4F-50-" + + "49-55-41-58-51-57-45-4F-49-55-3B-20-6D-61-78-2D-" + + "61-67-65-3D-33-36-30-30-3B-20-76-65-72-73-69-6F-" + + "6E-3D-31", hex); + + entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Content-Encoding", e.Name); + Assert.Equal("gzip", e.Value); + Assert.Equal(52u, e.Size); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:22 GMT", e.Value); + Assert.Equal(65u, e.Size); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + Assert.Equal(42u, e.Size); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("你好你好你好你.c", e.Value); + Assert.Equal(63u, e.Size); + }); + + Assert.Equal(222u, hpackEncoder.TableSize); } [Theory] diff --git a/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs b/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs index 72c3310358ec..8e1047f7b411 100644 --- a/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; using Xunit; diff --git a/src/Servers/Kestrel/Core/test/Http3HeadersEnumeratorTests.cs b/src/Servers/Kestrel/Core/test/Http3HeadersEnumeratorTests.cs new file mode 100644 index 000000000000..100aba3b6f3a --- /dev/null +++ b/src/Servers/Kestrel/Core/test/Http3HeadersEnumeratorTests.cs @@ -0,0 +1,156 @@ +// 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 System.Collections.Generic; +using System.Linq; +using System.Net.Http.HPack; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Microsoft.Extensions.Primitives; + +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http3HeadersEnumeratorTests + { + [Fact] + public void CanIterateOverResponseHeaders() + { + var responseHeaders = (IHeaderDictionary)new HttpResponseHeaders(); + + responseHeaders.ContentLength = 9; + responseHeaders.AcceptRanges = "AcceptRanges!"; + responseHeaders.Age = new StringValues(new[] { "1", "2" }); + responseHeaders.Date = "Date!"; + responseHeaders.GrpcEncoding = "Identity!"; + + responseHeaders.Append("Name1", "Value1"); + responseHeaders.Append("Name2", "Value2-1"); + responseHeaders.Append("Name2", "Value2-2"); + responseHeaders.Append("Name3", "Value3"); + + var e = new Http3HeadersEnumerator(); + e.Initialize(responseHeaders); + + var headers = GetNormalizedHeaders(e); + + Assert.Equal(new[] + { + CreateHeaderResult(-1, "Date", "Date!"), + CreateHeaderResult(-1, "Accept-Ranges", "AcceptRanges!"), + CreateHeaderResult(-1, "Age", "1"), + CreateHeaderResult(-1, "Age", "2"), + CreateHeaderResult(-1, "Grpc-Encoding", "Identity!"), + CreateHeaderResult(-1, "Content-Length", "9"), + CreateHeaderResult(-1, "Name1", "Value1"), + CreateHeaderResult(-1, "Name2", "Value2-1"), + CreateHeaderResult(-1, "Name2", "Value2-2"), + CreateHeaderResult(-1, "Name3", "Value3"), + }, headers); + } + + [Fact] + public void CanIterateOverResponseTrailers() + { + var responseTrailers = (IHeaderDictionary)new HttpResponseTrailers(); + + responseTrailers.ContentLength = 9; + responseTrailers.ETag = "ETag!"; + + responseTrailers.Append("Name1", "Value1"); + responseTrailers.Append("Name2", "Value2-1"); + responseTrailers.Append("Name2", "Value2-2"); + responseTrailers.Append("Name3", "Value3"); + + var e = new Http3HeadersEnumerator(); + e.Initialize(responseTrailers); + + var headers = GetNormalizedHeaders(e); + + Assert.Equal(new[] + { + CreateHeaderResult(-1, "ETag", "ETag!"), + CreateHeaderResult(-1, "Name1", "Value1"), + CreateHeaderResult(-1, "Name2", "Value2-1"), + CreateHeaderResult(-1, "Name2", "Value2-2"), + CreateHeaderResult(-1, "Name3", "Value3"), + }, headers); + } + + [Fact] + public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource() + { + var responseHeaders = new HttpResponseHeaders(); + responseHeaders.Append("Name1", "Value1"); + responseHeaders.Append("Name2", "Value2-1"); + responseHeaders.Append("Name2", "Value2-2"); + + var e = new Http3HeadersEnumerator(); + e.Initialize(responseHeaders); + + Assert.True(e.MoveNext()); + Assert.Equal("Name1", e.Current.Key); + Assert.Equal("Value1", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.True(e.MoveNext()); + Assert.Equal("Name2", e.Current.Key); + Assert.Equal("Value2-1", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.True(e.MoveNext()); + Assert.Equal("Name2", e.Current.Key); + Assert.Equal("Value2-2", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + var responseTrailers = (IHeaderDictionary)new HttpResponseTrailers(); + + responseTrailers.GrpcStatus = "1"; + + responseTrailers.Append("Name1", "Value1"); + responseTrailers.Append("Name2", "Value2-1"); + responseTrailers.Append("Name2", "Value2-2"); + + e.Initialize(responseTrailers); + + Assert.True(e.MoveNext()); + Assert.Equal("Grpc-Status", e.Current.Key); + Assert.Equal("1", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.True(e.MoveNext()); + Assert.Equal("Name1", e.Current.Key); + Assert.Equal("Value1", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.True(e.MoveNext()); + Assert.Equal("Name2", e.Current.Key); + Assert.Equal("Value2-1", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.True(e.MoveNext()); + Assert.Equal("Name2", e.Current.Key); + Assert.Equal("Value2-2", e.Current.Value); + Assert.Equal(-1, e.QPackStaticTableId); + + Assert.False(e.MoveNext()); + } + + private (int QPackStaticTableId, string Name, string Value)[] GetNormalizedHeaders(Http3HeadersEnumerator enumerator) + { + var headers = new List<(int HPackStaticTableId, string Name, string Value)>(); + while (enumerator.MoveNext()) + { + headers.Add(CreateHeaderResult(enumerator.QPackStaticTableId, enumerator.Current.Key, enumerator.Current.Value)); + } + return headers.ToArray(); + } + + private static (int QPackStaticTableId, string Key, string Value) CreateHeaderResult(int hPackStaticTableId, string key, string value) + { + return (hPackStaticTableId, key, value); + } + } +} diff --git a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs index a04a497271ec..de21074fbd81 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO.Pipelines; +using System.Text; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -129,6 +130,117 @@ public void AddingControlOrNonAsciiCharactersToHeadersThrows(string key, string }); } + [Theory] + [InlineData("\r\nData")] + [InlineData("\0Data")] + [InlineData("Data\r")] + [InlineData("Da\0ta")] + [InlineData("Da\u001Fta")] + [InlineData("Data\0")] + [InlineData("Da\nta")] + [InlineData("Da\u007Fta")] + [InlineData("Da\u0080ta")] + [InlineData("Da™ta")] + [InlineData("Dašta")] + public void AddingControlOrNonAsciiCharactersToHeaderPropertyThrows(string value) + { + var responseHeaders = (IHeaderDictionary)new HttpResponseHeaders(); + + // Known special header + Assert.Throws(() => + { + responseHeaders.Allow = value; + }); + + // Unknown header fallback + Assert.Throws(() => + { + responseHeaders.Accept = value; + }); + } + + [Theory] + [InlineData("\r\nData")] + [InlineData("\0Data")] + [InlineData("Data\r")] + [InlineData("Da\0ta")] + [InlineData("Da\u001Fta")] + [InlineData("Data\0")] + [InlineData("Da\nta")] + [InlineData("Da\u007Fta")] + public void AddingControlCharactersWithCustomEncoderThrows(string value) + { + var responseHeaders = new HttpResponseHeaders(_ => Encoding.UTF8); + + // Known special header + Assert.Throws(() => + { + ((IHeaderDictionary)responseHeaders).Allow = value; + }); + + // Unknown header fallback + Assert.Throws(() => + { + ((IHeaderDictionary)responseHeaders).Accept = value; + }); + + Assert.Throws(() => + { + ((IHeaderDictionary)responseHeaders)["Unknown"] = value; + }); + + Assert.Throws(() => + { + ((IHeaderDictionary)responseHeaders)["Unknown"] = new StringValues(new[] { "valid", value }); + }); + + Assert.Throws(() => + { + ((IDictionary)responseHeaders)["Unknown"] = value; + }); + + Assert.Throws(() => + { + var kvp = new KeyValuePair("Unknown", value); + ((ICollection>)responseHeaders).Add(kvp); + }); + + Assert.Throws(() => + { + var kvp = new KeyValuePair("Unknown", value); + ((IDictionary)responseHeaders).Add("Unknown", value); + }); + } + + [Theory] + [InlineData("Da\u0080ta")] + [InlineData("Da™ta")] + [InlineData("Dašta")] + public void AddingNonAsciiCharactersWithCustomEncoderWorks(string value) + { + var responseHeaders = new HttpResponseHeaders(_ => Encoding.UTF8); + + // Known special header + ((IHeaderDictionary)responseHeaders).Allow = value; + + // Unknown header fallback + ((IHeaderDictionary)responseHeaders).Accept = value; + + ((IHeaderDictionary)responseHeaders)["Unknown"] = value; + + ((IHeaderDictionary)responseHeaders)["Unknown"] = new StringValues(new[] { "valid", value }); + + ((IDictionary)responseHeaders)["Unknown"] = value; + + ((IHeaderDictionary)responseHeaders).Clear(); + var kvp = new KeyValuePair("Unknown", value); + ((ICollection>)responseHeaders).Add(kvp); + + ((IHeaderDictionary)responseHeaders).Clear(); + kvp = new KeyValuePair("Unknown", value); + ((IDictionary)responseHeaders).Add("Unknown", value); + } + [Fact] public void ThrowsWhenAddingHeaderAfterReadOnlyIsSet() { diff --git a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs index 7df5b8f4efdf..258b0123a919 100644 --- a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs +++ b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs @@ -148,8 +148,7 @@ public void WriteAscii() } [Theory] - [InlineData(2, 1)] - [InlineData(3, 1)] + [InlineData(3, 2)] [InlineData(4, 2)] [InlineData(5, 3)] [InlineData(7, 4)] diff --git a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs index 5083ba7d16f5..cb4225bb50ca 100644 --- a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs +++ b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs @@ -18,7 +18,7 @@ public class UTF8DecodingTests [InlineData(new byte[] { 0xef, 0xbf, 0xbd })] // 3 bytes: Replacement character, highest UTF-8 character currently encoded in the UTF-8 code page private void FullUTF8RangeSupported(byte[] encodedBytes) { - var s = HttpUtilities.GetRequestHeaderString(encodedBytes.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultRequestHeaderEncodingSelector); + var s = HttpUtilities.GetRequestHeaderString(encodedBytes.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultHeaderEncodingSelector); Assert.Equal(1, s.Length); } @@ -37,7 +37,7 @@ private void ExceptionThrownForZeroOrNonAscii(byte[] bytes) Array.Copy(bytes, 0, byteRange, position, bytes.Length); Assert.Throws(() => - HttpUtilities.GetRequestHeaderString(byteRange.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)); + HttpUtilities.GetRequestHeaderString(byteRange.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultHeaderEncodingSelector)); } } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/BytesToStringBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/BytesToStringBenchmark.cs index 0258e902ee79..a1e803d411ba 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/BytesToStringBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/BytesToStringBenchmark.cs @@ -74,7 +74,7 @@ public void Utf8BytesToString() { for (uint i = 0; i < Iterations; i++) { - HttpUtilities.GetRequestHeaderString(_utf8Bytes, _headerName, KestrelServerOptions.DefaultRequestHeaderEncodingSelector); + HttpUtilities.GetRequestHeaderString(_utf8Bytes, _headerName, KestrelServerOptions.DefaultHeaderEncodingSelector); } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HPackHeaderWriterBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HPackHeaderWriterBenchmark.cs index b78d3e511e60..751129e0ffa9 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HPackHeaderWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HPackHeaderWriterBenchmark.cs @@ -18,8 +18,8 @@ public class HPackHeaderWriterBenchmark { private Http2HeadersEnumerator _http2HeadersEnumerator; private DynamicHPackEncoder _hpackEncoder; - private IHeaderDictionary _knownResponseHeaders; - private IHeaderDictionary _unknownResponseHeaders; + private HttpResponseHeaders _knownResponseHeaders; + private HttpResponseHeaders _unknownResponseHeaders; private byte[] _buffer; [GlobalSetup] @@ -31,18 +31,19 @@ public void GlobalSetup() _knownResponseHeaders = new HttpResponseHeaders(); - _knownResponseHeaders.Server = "Kestrel"; - _knownResponseHeaders.ContentType = "application/json"; - _knownResponseHeaders.Date = "Date!"; - _knownResponseHeaders.ContentLength = 0; - _knownResponseHeaders.AcceptRanges = "Ranges!"; - _knownResponseHeaders.TransferEncoding = "Encoding!"; - _knownResponseHeaders.Via = "Via!"; - _knownResponseHeaders.Vary = "Vary!"; - _knownResponseHeaders.WWWAuthenticate = "Authenticate!"; - _knownResponseHeaders.LastModified = "Modified!"; - _knownResponseHeaders.Expires = "Expires!"; - _knownResponseHeaders.Age = "Age!"; + var knownHeaders = (IHeaderDictionary)_knownResponseHeaders; + knownHeaders.Server = "Kestrel"; + knownHeaders.ContentType = "application/json"; + knownHeaders.Date = "Date!"; + knownHeaders.ContentLength = 0; + knownHeaders.AcceptRanges = "Ranges!"; + knownHeaders.TransferEncoding = "Encoding!"; + knownHeaders.Via = "Via!"; + knownHeaders.Vary = "Vary!"; + knownHeaders.WWWAuthenticate = "Authenticate!"; + knownHeaders.LastModified = "Modified!"; + knownHeaders.Expires = "Expires!"; + knownHeaders.Age = "Age!"; _unknownResponseHeaders = new HttpResponseHeaders(); for (var i = 0; i < 10; i++) @@ -58,11 +59,27 @@ public void BeginEncodeHeaders_KnownHeaders() HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); } + [Benchmark] + public void BeginEncodeHeaders_KnownHeaders_CustomEncoding() + { + _knownResponseHeaders.EncodingSelector = _ => Encoding.UTF8; + _http2HeadersEnumerator.Initialize(_knownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } + [Benchmark] public void BeginEncodeHeaders_UnknownHeaders() { _http2HeadersEnumerator.Initialize(_unknownResponseHeaders); HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); } + + [Benchmark] + public void BeginEncodeHeaders_UnknownHeaders_CustomEncoding() + { + _knownResponseHeaders.EncodingSelector = _ => Encoding.UTF8; + _http2HeadersEnumerator.Initialize(_unknownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs index 11eac3dd5859..9bcb669c373f 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs @@ -52,8 +52,8 @@ public void ResponseMinimumDataRateNotSatisfied(string connectionId, string trac public void ApplicationAbortedConnection(string connectionId, string traceIdentifier) { } public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { } public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { } - public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) { } - public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex) { } + public void HPackDecodingError(string connectionId, int streamId, Exception ex) { } + public void HPackEncodingError(string connectionId, int streamId, Exception ex) { } public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason) { } public void Http2ConnectionClosing(string connectionId) { } public void Http2ConnectionClosed(string connectionId, int highestOpenedStreamId) { } @@ -67,8 +67,8 @@ public void Http3ConnectionClosed(string connectionId, long highestOpenedStreamI public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, ConnectionAbortedException abortReason) { } public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame frame) { } public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame frame) { } - public void QPackDecodingError(string connectionId, long streamId, QPackDecodingException ex) { } - public void QPackEncodingError(string connectionId, long streamId, QPackEncodingException ex) { } + public void QPackDecodingError(string connectionId, long streamId, Exception ex) { } + public void QPackEncodingError(string connectionId, long streamId, Exception ex) { } public void Http3OutboundControlStreamError(string connectionId, Exception ex) { } } } diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 9ccbc432fb12..6c0d60d6b092 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -329,13 +329,15 @@ static string AppendHPackSwitchSection(HPackGroup group) var header = group.Header; if (header.Name == HeaderNames.ContentLength) { - return $@"if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + return $@"var customEncoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : EncodingSelector(HeaderNames.ContentLength); + if (customEncoding == null) {{ AppendContentLength(value); }} else {{ - AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + AppendContentLengthCustomEncoding(value, customEncoding); }} return true;"; } @@ -370,13 +372,15 @@ string GenerateIfBody(KnownHeader header, string extraIndent = "") if (header.Name == HeaderNames.ContentLength) { return $@" - {extraIndent}if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + {extraIndent}var customEncoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + {extraIndent} ? null : EncodingSelector(HeaderNames.ContentLength); + {extraIndent}if (customEncoding == null) {extraIndent}{{ {extraIndent} AppendContentLength(value); {extraIndent}}} {extraIndent}else {extraIndent}{{ - {extraIndent} AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + {extraIndent} AppendContentLengthCustomEncoding(value, customEncoding); {extraIndent}}} {extraIndent}return;"; } @@ -775,6 +779,7 @@ public static string GeneratedFile() using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.Http; @@ -858,7 +863,8 @@ StringValues IHeaderDictionary.{header.Identifier} var flag = {header.FlagBit()}; if (value.Count > 0) - {{ + {{{(loop.ClassName != "HttpRequestHeaders" ? $@" + ValidateHeaderValueCharacters(HeaderNames.{header.Identifier}, value, EncodingSelector);" : "")} _bits |= flag; _headers._{header.Identifier} = value; }} @@ -884,8 +890,8 @@ StringValues IHeaderDictionary.{header} }} set {{ - if (_isReadOnly) {{ ThrowHeadersReadOnlyException(); }} - + if (_isReadOnly) {{ ThrowHeadersReadOnlyException(); }}{(loop.ClassName != "HttpRequestHeaders" ? $@" + ValidateHeaderValueCharacters(HeaderNames.{header}, value, EncodingSelector);" : "")} SetValueUnknown(HeaderNames.{header}, value); }} }}")} @@ -948,7 +954,7 @@ protected override bool TryGetValueFast(string key, out StringValues value) protected override void SetValueFast(string key, StringValues value) {{{(loop.ClassName != "HttpRequestHeaders" ? @" - ValidateHeaderValueCharacters(value);" : "")} + ValidateHeaderValueCharacters(key, value, EncodingSelector);" : "")} switch (key.Length) {{{Each(loop.HeadersByLength, byLength => $@" case {byLength.Key}: @@ -979,7 +985,7 @@ protected override void SetValueFast(string key, StringValues value) protected override bool AddValueFast(string key, StringValues value) {{{(loop.ClassName != "HttpRequestHeaders" ? @" - ValidateHeaderValueCharacters(value);" : "")} + ValidateHeaderValueCharacters(key, value, EncodingSelector);" : "")} switch (key.Length) {{{Each(loop.HeadersByLength, byLength => $@" case {byLength.Key}: @@ -1171,6 +1177,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) {{ int keyStart; int keyLength; + var headerName = string.Empty; switch (next) {{{Each(loop.Headers.OrderBy(h => h.Index).Where(h => h.Identifier != "ContentLength"), header => $@" case {header.Index}: // Header: ""{header.Name}"" @@ -1191,6 +1198,7 @@ internal unsafe void CopyToFast(ref BufferWriter output) values = ref _headers._{header.Identifier}; keyStart = {header.BytesOffset}; keyLength = {header.BytesCount}; + headerName = HeaderNames.{header.Identifier}; }}")} break; // OutputHeader ")} @@ -1203,6 +1211,8 @@ internal unsafe void CopyToFast(ref BufferWriter output) {{ // Clear bit tempBits ^= (1UL << next); + var encoding = ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : EncodingSelector(headerName); var valueCount = values.Count; Debug.Assert(valueCount > 0); @@ -1213,7 +1223,14 @@ internal unsafe void CopyToFast(ref BufferWriter output) if (value != null) {{ output.Write(headerKey); - output.WriteAscii(value); + if (encoding is null) + {{ + output.WriteAscii(value); + }} + else + {{ + output.WriteEncoded(value, encoding); + }} }} }} // Set exact next diff --git a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs index f106506b4e46..a8f7ca03a99d 100644 --- a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs +++ b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs @@ -192,13 +192,13 @@ public void Http2StreamError(string connectionId, Http2StreamErrorException ex) _trace2.Http2StreamError(connectionId, ex); } - public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) + public void HPackDecodingError(string connectionId, int streamId, Exception ex) { _trace1.HPackDecodingError(connectionId, streamId, ex); _trace2.HPackDecodingError(connectionId, streamId, ex); } - public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex) + public void HPackEncodingError(string connectionId, int streamId, Exception ex) { _trace1.HPackEncodingError(connectionId, streamId, ex); _trace2.HPackEncodingError(connectionId, streamId, ex); @@ -282,13 +282,13 @@ public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame _trace2.Http3FrameSending(connectionId, streamId, frame); } - public void QPackDecodingError(string connectionId, long streamId, QPackDecodingException ex) + public void QPackDecodingError(string connectionId, long streamId, Exception ex) { _trace1.QPackDecodingError(connectionId, streamId, ex); _trace2.QPackDecodingError(connectionId, streamId, ex); } - public void QPackEncodingError(string connectionId, long streamId, QPackEncodingException ex) + public void QPackEncodingError(string connectionId, long streamId, Exception ex) { _trace1.QPackEncodingError(connectionId, streamId, ex); _trace2.QPackEncodingError(connectionId, streamId, ex); diff --git a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs index 11e7925bf52d..f5b41b10185a 100644 --- a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs +++ b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs @@ -20,10 +20,10 @@ public abstract class StreamBackedTestConnection : IDisposable private readonly Stream _stream; private readonly StreamReader _reader; - protected StreamBackedTestConnection(Stream stream) + protected StreamBackedTestConnection(Stream stream, Encoding encoding = null) { _stream = stream; - _reader = new StreamReader(_stream, Encoding.ASCII); + _reader = new StreamReader(_stream, encoding ?? Encoding.ASCII); } public Stream Stream => _stream; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 3e0850b4c75b..5e4e6e2f5ea6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -1953,6 +1953,105 @@ await InitializeConnectionAsync(async context => Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); } + [Fact] + public async Task ResponseHeaders_WithNonAscii_Throws() + { + await InitializeConnectionAsync(async context => + { + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.ContentType = "Custom 你好 Type"); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom 你好 Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + await context.Response.WriteAsync("Hello World"); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + } + + [Fact] + public async Task ResponseHeaders_WithNonAsciiAndCustomEncoder_Works() + { + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; + _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. + + await InitializeConnectionAsync(async context => + { + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + context.Response.ContentType = "Custom 你好 Type"; + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + await context.Response.WriteAsync("Hello World"); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 84, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(4, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("Custom 你好 Type", _decodedHeaders[HeaderNames.ContentType]); + Assert.Equal("Custom 你好 Value", _decodedHeaders["CustomName"]); + } + + [Fact] + public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnection() + { + var encoding = Encoding.GetEncoding(Encoding.Latin1.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; + + await InitializeConnectionAsync(async context => + { + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + await context.Response.WriteAsync("Hello World"); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + } + [Fact] public async Task ResponseTrailers_WithoutData_Sent() { @@ -2185,6 +2284,10 @@ await InitializeConnectionAsync(async context => await context.Response.WriteAsync("Hello World"); Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom 你好 Value")); + Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom \r Value")); + // ETag is one of the few special cased trailers. Accept is not. + Assert.Throws(() => context.Features.Get().Trailers.ETag = "Custom 你好 Tag"); + Assert.Throws(() => context.Features.Get().Trailers.Accept = "Custom 你好 Tag"); }); await StartStreamAsync(1, _browserRequestHeaders, endStream: true); @@ -2213,6 +2316,92 @@ await ExpectAsync(Http2FrameType.DATA, Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); } + [Fact] + public async Task ResponseTrailers_WithNonAsciiAndCustomEncoder_Works() + { + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; + _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. + + await InitializeConnectionAsync(async context => + { + await context.Response.WriteAsync("Hello World"); + Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom \r Value")); + context.Response.AppendTrailer("CustomName", "Custom 你好 Value"); + // ETag is one of the few special cased trailers. Accept is not. + context.Features.Get().Trailers.ETag = "Custom 你好 Tag"; + context.Features.Get().Trailers.Accept = "Custom 你好 Accept"; + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 80, + withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + + _decodedHeaders.Clear(); + + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Equal("Custom 你好 Value", _decodedHeaders["CustomName"]); + Assert.Equal("Custom 你好 Tag", _decodedHeaders[HeaderNames.ETag]); + Assert.Equal("Custom 你好 Accept", _decodedHeaders[HeaderNames.Accept]); + } + + [Fact] + public async Task ResponseTrailers_WithInvalidValuesAndCustomEncoder_AbortsConnection() + { + var encoding = Encoding.GetEncoding(Encoding.Latin1.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; + + await InitializeConnectionAsync(async context => + { + await context.Response.WriteAsync("Hello World"); + context.Response.AppendTrailer("CustomName", "Custom 你好 Value"); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + + await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + } + [Fact] public async Task ResponseTrailers_TooLong_Throws() { @@ -4787,6 +4976,25 @@ await WaitForConnectionErrorAsync( expectedErrorMessage: CoreStrings.BadRequest_MalformedRequestInvalidHeaders); } + [Fact] + public async Task HEADERS_Received_CustomEncoding_InvalidCharacters_AbortsConnection() + { + var encoding = Encoding.GetEncoding(Encoding.ASCII.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => encoding; + + await InitializeConnectionAsync(context => + { + Assert.Equal("£", context.Request.Headers["X-Test"]); + return Task.CompletedTask; + }); + + await StartStreamAsync(1, LatinHeaderData, endStream: true); + + await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, + Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.BadRequest_MalformedRequestInvalidHeaders); + } + [Fact] public async Task RemoveConnectionSpecificHeaders() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index af7ec5dc7fb0..838c21f5f401 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -736,6 +736,103 @@ public async Task ResponseTrailers_WithoutData_Sent() Assert.Equal("Value2", responseTrailers["Trailer2"]); } + [Fact] + public async Task ResponseHeaders_WithNonAscii_Throws() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + var trailersFeature = context.Features.Get(); + + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.ContentType = "Custom 你好 Type"); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom 你好 Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + await context.Response.WriteAsync("Hello World"); + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); + + Assert.Equal(2, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + } + + [Fact] + public async Task ResponseHeaders_WithNonAsciiAndCustomEncoder_Works() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; + _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + var trailersFeature = context.Features.Get(); + + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + context.Response.ContentType = "Custom 你好 Type"; + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + await context.Response.WriteAsync("Hello World"); + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("Custom 你好 Type", responseHeaders[HeaderNames.ContentType]); + Assert.Equal("Custom 你好 Value", responseHeaders["CustomName"]); + } + + [Fact] + public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnection() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var encoding = Encoding.GetEncoding(Encoding.Latin1.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + await context.Response.WriteAsync("Hello World"); + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.InternalError, ""); + } + [Fact] public async Task ResponseTrailers_WithData_Sent() { @@ -798,6 +895,106 @@ public async Task ResponseTrailers_WithExeption500_Cleared() await requestStream.ExpectReceiveEndOfStream(); } + [Fact] + public async Task ResponseTrailers_WithNonAscii_Throws() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + await context.Response.WriteAsync("Hello World"); + Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom 你好 Value")); + Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom \r Value")); + // ETag is one of the few special cased trailers. Accept is not. + Assert.Throws(() => context.Features.Get().Trailers.ETag = "Custom 你好 Tag"); + Assert.Throws(() => context.Features.Get().Trailers.Accept = "Custom 你好 Tag"); + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); + await requestStream.ExpectReceiveEndOfStream(); + } + + [Fact] + public async Task ResponseTrailers_WithNonAsciiAndCustomEncoder_Works() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; + _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + await context.Response.WriteAsync("Hello World"); + Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.AppendTrailer("CustomName", "Custom \r Value")); + context.Response.AppendTrailer("CustomName", "Custom 你好 Value"); + // ETag is one of the few special cased trailers. Accept is not. + context.Features.Get().Trailers.ETag = "Custom 你好 Tag"; + context.Features.Get().Trailers.Accept = "Custom 你好 Accept"; + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); + + var responseTrailers = await requestStream.ExpectHeadersAsync(); + Assert.Equal(3, responseTrailers.Count); + Assert.Equal("Custom 你好 Value", responseTrailers["CustomName"]); + Assert.Equal("Custom 你好 Tag", responseTrailers[HeaderNames.ETag]); + Assert.Equal("Custom 你好 Accept", responseTrailers[HeaderNames.Accept]); + + await requestStream.ExpectReceiveEndOfStream(); + } + + [Fact] + public async Task ResponseTrailers_WithInvalidValuesAndCustomEncoder_AbortsConnection() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var encoding = Encoding.GetEncoding(Encoding.Latin1.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + await context.Response.WriteAsync("Hello World"); + context.Response.AppendTrailer("CustomName", "Custom 你好 Value"); + }); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.InternalError, ""); + } + [Fact] public async Task ResetStream_ReturnStreamError() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index cf388abededa..3d34c7443f7a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -27,6 +27,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -620,12 +621,24 @@ public async Task SendHeadersAsync(IEnumerable> hea var frame = new Http3RawFrame(); frame.PrepareHeaders(); var buffer = _headerEncodingBuffer.AsMemory(); - var done = QPackHeaderWriter.BeginEncode(headers.GetEnumerator(), buffer.Span, ref headersTotalSize, out var length); + var done = QPackHeaderWriter.BeginEncode(GetHeadersEnumerator(headers), + buffer.Span, ref headersTotalSize, out var length); Assert.True(done); await SendFrameAsync(frame, buffer.Slice(0, length), endStream); } + internal Http3HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) + { + var dictionary = headers + .GroupBy(g => g.Key) + .ToDictionary(g => g.Key, g => new StringValues(g.Select(values => values.Value).ToArray())); + + var headersEnumerator = new Http3HeadersEnumerator(); + headersEnumerator.Initialize(dictionary); + return headersEnumerator; + } + internal async Task SendHeadersPartialAsync() { // Send HEADERS frame header without content. diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseHeaderTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseHeaderTests.cs new file mode 100644 index 000000000000..86fa7a1fa2ac --- /dev/null +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseHeaderTests.cs @@ -0,0 +1,104 @@ +// 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 System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests +{ + public class ResponseHeaderTests : TestApplicationErrorLoggerLoggedTest + { + [Fact] + public async Task ResponseHeaders_WithNonAscii_Throws() + { + await using var server = new TestServer(context => + { + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.ContentType = "Custom 你好 Type"); // Special cased + Assert.Throws(() => context.Response.Headers.Accept = "Custom 你好 Accept"); // Not special cased + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom 你好 Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + context.Response.ContentLength = 11; + return context.Response.WriteAsync("Hello World"); + }, new TestServiceContext(LoggerFactory)); + using var connection = server.CreateConnection(); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + $"HTTP/1.1 200 OK", + "Content-Length: 11", + $"Date: {server.Context.DateHeaderValue}", + "", + "Hello World"); + } + + [Fact] + public async Task ResponseHeaders_WithNonAsciiWithCustomEncoding_Works() + { + var testContext = new TestServiceContext(LoggerFactory); + testContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; + + await using var server = new TestServer(context => + { + Assert.Throws(() => context.Response.Headers.Append("Custom你好Name", "Custom Value")); + Assert.Throws(() => context.Response.Headers.Append("CustomName", "Custom \r Value")); + context.Response.ContentType = "Custom 你好 Type"; + context.Response.Headers.Accept = "Custom 你好 Accept"; + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + context.Response.ContentLength = 11; + return context.Response.WriteAsync("Hello World"); + }, testContext); + + using var connection = server.CreateConnection(Encoding.UTF8); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + $"HTTP/1.1 200 OK", + "Content-Length: 11", + "Content-Type: Custom 你好 Type", + $"Date: {server.Context.DateHeaderValue}", + "Accept: Custom 你好 Accept", + "CustomName: Custom 你好 Value", + "", + "Hello World"); + } + + [Fact] + public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnection() + { + var testContext = new TestServiceContext(LoggerFactory); + var encoding = Encoding.GetEncoding(Encoding.Latin1.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + testContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; + + await using var server = new TestServer(context => + { + context.Response.Headers.Append("CustomName", "Custom 你好 Value"); + context.Response.ContentLength = 11; + return context.Response.WriteAsync("Hello World"); + }, testContext); + using var connection = server.CreateConnection(); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.ReceiveEnd(); + } + } +} diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs index b8e3209f8887..4eccebe2e0df 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs @@ -1,6 +1,7 @@ // 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 System.Text; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Testing; @@ -9,8 +10,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans { internal class InMemoryConnection : StreamBackedTestConnection { - public InMemoryConnection(InMemoryTransportConnection transportConnection) - : base(new DuplexPipeStream(transportConnection.Output, transportConnection.Input)) + public InMemoryConnection(InMemoryTransportConnection transportConnection, Encoding encoding) + : base(new DuplexPipeStream(transportConnection.Output, transportConnection.Input), encoding) { TransportConnection = transportConnection; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 21ca585fed2c..8707ec747fe2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -115,11 +116,11 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action _headerTableSize; + public DynamicHPackEncoder(bool allowDynamicCompression = true, uint maxHeaderTableSize = DefaultHeaderTableSize) { _allowDynamicCompression = allowDynamicCompression; _maxHeaderTableSize = maxHeaderTableSize; Head = new EncoderHeaderEntry(); - Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null); + Head.Initialize(-1, string.Empty, string.Empty, 0, int.MaxValue, null); // Bucket count balances memory usage and the expected low number of headers (constrained by the header table size). // Performance with different bucket counts hasn't been measured in detail. _headerBuckets = new EncoderHeaderEntry[16]; @@ -63,7 +66,8 @@ public bool EnsureDynamicTableSizeUpdate(Span buffer, out int length) return true; } - public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, out int bytesWritten) + public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, + Encoding? valueEncoding, out int bytesWritten) { Debug.Assert(!_pendingTableSizeUpdate, "Dynamic table size update should be encoded before headers."); @@ -73,30 +77,31 @@ public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncoding int index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten); + ? HPackEncoder.EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, valueEncoding, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldNeverIndexing(index, value, valueEncoding, buffer, out bytesWritten); } // No dynamic table. Only use the static table. if (!_allowDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex) { return staticTableIndex == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten); + ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, valueEncoding, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, valueEncoding, buffer, out bytesWritten); } // Header is greater than the maximum table size. // Don't attempt to add dynamic header as all existing dynamic headers will be removed. - if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize) + var headerLength = HeaderField.GetLength(name.Length, valueEncoding?.GetByteCount(value) ?? value.Length); + if (headerLength > _maxHeaderTableSize) { int index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten); + ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, valueEncoding, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(index, value, valueEncoding, buffer, out bytesWritten); } - return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten); + return EncodeDynamicHeader(buffer, staticTableIndex, name, value, headerLength, valueEncoding, out bytesWritten); } private int ResolveDynamicTableIndex(int staticTableIndex, string name) @@ -110,7 +115,8 @@ private int ResolveDynamicTableIndex(int staticTableIndex, string name) return CalculateDynamicTableIndex(name); } - private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) + private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, + int headerLength, Encoding? valueEncoding, out int bytesWritten) { EncoderHeaderEntry? headerField = GetEntry(name, value); if (headerField != null) @@ -122,15 +128,15 @@ private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string else { // Doesn't exist in dynamic table. Add new entry to dynamic table. - uint headerSize = (uint)HeaderField.GetLength(name.Length, value.Length); int index = ResolveDynamicTableIndex(staticTableIndex, name); bool success = index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); + ? HPackEncoder.EncodeLiteralHeaderFieldIndexingNewName(name, value, valueEncoding, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldIndexing(index, value, valueEncoding, buffer, out bytesWritten); if (success) { + uint headerSize = (uint)headerLength; EnsureCapacity(headerSize); AddHeaderEntry(name, value, headerSize); } @@ -212,7 +218,7 @@ private void AddHeaderEntry(string name, string value, uint headerSize) EncoderHeaderEntry? oldEntry = _headerBuckets[bucketIndex]; // Attempt to reuse removed entry EncoderHeaderEntry? newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry(); - newEntry.Initialize(hash, name, value, Head.Before!.Index - 1, oldEntry); + newEntry.Initialize(hash, name, value, headerSize, Head.Before!.Index - 1, oldEntry); _headerBuckets[bucketIndex] = newEntry; newEntry.AddBefore(Head); _headerTableSize += headerSize; @@ -266,7 +272,7 @@ private void PushRemovedEntry(EncoderHeaderEntry removed) { prev.Next = next; } - _headerTableSize -= eldest.CalculateSize(); + _headerTableSize -= eldest.Size; eldest.Remove(); return eldest; } diff --git a/src/Shared/Hpack/EncoderHeaderEntry.cs b/src/Shared/Hpack/EncoderHeaderEntry.cs index aa87ab7dcd91..646fa19f6ca3 100644 --- a/src/Shared/Hpack/EncoderHeaderEntry.cs +++ b/src/Shared/Hpack/EncoderHeaderEntry.cs @@ -12,6 +12,7 @@ internal class EncoderHeaderEntry // Header name and value public string? Name; public string? Value; + public uint Size; // Chained list of headers in the same bucket public EncoderHeaderEntry? Next; @@ -27,23 +28,19 @@ internal class EncoderHeaderEntry /// /// Initialize header values. An entry will be reinitialized when reused. /// - public void Initialize(int hash, string name, string value, int index, EncoderHeaderEntry? next) + public void Initialize(int hash, string name, string value, uint size, int index, EncoderHeaderEntry? next) { Debug.Assert(name != null); Debug.Assert(value != null); Name = name; Value = value; + Size = size; Index = index; Hash = hash; Next = next; } - public uint CalculateSize() - { - return (uint)HeaderField.GetLength(Name!.Length, Value!.Length); - } - /// /// Remove entry from the linked list and reset header values. /// @@ -57,6 +54,7 @@ public void Remove() Hash = 0; Name = null; Value = null; + Size = 0; } /// diff --git a/src/Shared/Http2cat/HPackHeaderWriter.cs b/src/Shared/Http2cat/HPackHeaderWriter.cs index 27772caa7231..33e66b4befcf 100644 --- a/src/Shared/Http2cat/HPackHeaderWriter.cs +++ b/src/Shared/Http2cat/HPackHeaderWriter.cs @@ -85,7 +85,7 @@ private static bool EncodeHeaders(IEnumerator> head private static bool EncodeHeader(string name, string value, Span buffer, out int length) { - return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out length); + return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, valueEncoding: null, buffer, out length); } } } diff --git a/src/Shared/ServerInfrastructure/BufferExtensions.cs b/src/Shared/ServerInfrastructure/BufferExtensions.cs index 6f3ef2461270..0d6b61779c0e 100644 --- a/src/Shared/ServerInfrastructure/BufferExtensions.cs +++ b/src/Shared/ServerInfrastructure/BufferExtensions.cs @@ -124,7 +124,7 @@ internal static void WriteAscii(ref this BufferWriter buffer, string } else { - WriteAsciiMultiWrite(ref buffer, data); + WriteEncodedMultiWrite(ref buffer, data, sourceLength, Encoding.ASCII); } } @@ -199,32 +199,61 @@ private static void WriteNumericMultiWrite(ref this BufferWriter buf buffer.Write(new ReadOnlySpan(byteBuffer, position, length)); } + internal static void WriteEncoded(ref this BufferWriter buffer, string data, Encoding encoding) + { + if (string.IsNullOrEmpty(data)) + { + return; + } + + var dest = buffer.Span; + var sourceLength = encoding.GetByteCount(data); + // Fast path, try encoding to the available memory directly + if (sourceLength <= dest.Length) + { + encoding.GetBytes(data, dest); + buffer.Advance(sourceLength); + } + else + { + WriteEncodedMultiWrite(ref buffer, data, sourceLength, encoding); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] - private static void WriteAsciiMultiWrite(ref this BufferWriter buffer, string data) + private static void WriteEncodedMultiWrite(ref this BufferWriter buffer, string data, int encodedLength, Encoding encoding) { - var dataLength = data.Length; - var offset = 0; + var source = data.AsSpan(); + var totalBytesUsed = 0; + var encoder = encoding.GetEncoder(); + var minBufferSize = encoding.GetMaxByteCount(1); + buffer.Ensure(minBufferSize); var bytes = buffer.Span; - do + var completed = false; + + // This may be a bug, but encoder.Convert returns completed = true for UTF7 too early. + // Therefore, we check encodedLength - totalBytesUsed too. + while (!completed || encodedLength - totalBytesUsed != 0) { - var writable = Math.Min(dataLength - offset, bytes.Length); // Zero length spans are possible, though unlikely. - // ASCII.GetBytes and .Advance will both handle them so we won't special case for them. - Encoding.ASCII.GetBytes(data.AsSpan(offset, writable), bytes); - buffer.Advance(writable); + // encoding.Convert and .Advance will both handle them so we won't special case for them. + encoder.Convert(source, bytes, flush: true, out var charsUsed, out var bytesUsed, out completed); + buffer.Advance(bytesUsed); - offset += writable; - if (offset >= dataLength) + totalBytesUsed += bytesUsed; + if (totalBytesUsed >= encodedLength) { - Debug.Assert(offset == dataLength); + Debug.Assert(totalBytesUsed == encodedLength); // Encoded everything break; } + source = source.Slice(charsUsed); + // Get new span, more to encode. - buffer.Ensure(); + buffer.Ensure(minBufferSize); bytes = buffer.Span; - } while (true); + } } private static byte[] NumericBytesScratch => _numericBytesScratch ?? CreateNumericBytesScratch(); diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs index 67a61c3c69f3..a7f33d2c9298 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs @@ -78,7 +78,7 @@ public static bool EncodeStatusHeader(int statusCode, Span destination, ou } /// Encodes a "Literal Header Field without Indexing". - public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.2 // ------------------------------------------------------ @@ -97,7 +97,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string val if (IntegerEncoder.Encode(index, 4, destination, out int indexLength)) { Debug.Assert(indexLength >= 1); - if (EncodeStringLiteral(value, valueEncoding: null, destination.Slice(indexLength), out int nameLength)) + if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength)) { bytesWritten = indexLength + nameLength; return true; @@ -110,7 +110,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string val } /// Encodes a "Literal Header Field never Indexing". - public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.3 // ------------------------------------------------------ @@ -129,7 +129,7 @@ public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value if (IntegerEncoder.Encode(index, 4, destination, out int indexLength)) { Debug.Assert(indexLength >= 1); - if (EncodeStringLiteral(value, valueEncoding: null, destination.Slice(indexLength), out int nameLength)) + if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength)) { bytesWritten = indexLength + nameLength; return true; @@ -142,7 +142,7 @@ public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value } /// Encodes a "Literal Header Field with Indexing". - public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.2 // ------------------------------------------------------ @@ -161,7 +161,7 @@ public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Spa if (IntegerEncoder.Encode(index, 6, destination, out int indexLength)) { Debug.Assert(indexLength >= 1); - if (EncodeStringLiteral(value, valueEncoding: null, destination.Slice(indexLength), out int nameLength)) + if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength)) { bytesWritten = indexLength + nameLength; return true; @@ -209,7 +209,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span } /// Encodes a "Literal Header Field with Indexing - New Name". - public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.2 // ------------------------------------------------------ @@ -226,11 +226,11 @@ public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string v // | Value String (Length octets) | // +-------------------------------+ - return EncodeLiteralHeaderNewNameCore(0x40, name, value, destination, out bytesWritten); + return EncodeLiteralHeaderNewNameCore(0x40, name, value, valueEncoding, destination, out bytesWritten); } /// Encodes a "Literal Header Field without Indexing - New Name". - public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.2 // ------------------------------------------------------ @@ -247,11 +247,11 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, s // | Value String (Length octets) | // +-------------------------------+ - return EncodeLiteralHeaderNewNameCore(0, name, value, destination, out bytesWritten); + return EncodeLiteralHeaderNewNameCore(0, name, value, valueEncoding, destination, out bytesWritten); } /// Encodes a "Literal Header Field never Indexing - New Name". - public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Span destination, out int bytesWritten) + public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-6.2.3 // ------------------------------------------------------ @@ -268,16 +268,16 @@ public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, str // | Value String (Length octets) | // +-------------------------------+ - return EncodeLiteralHeaderNewNameCore(0x10, name, value, destination, out bytesWritten); + return EncodeLiteralHeaderNewNameCore(0x10, name, value, valueEncoding, destination, out bytesWritten); } - private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Span destination, out int bytesWritten) + private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Encoding? valueEncoding, Span destination, out int bytesWritten) { if ((uint)destination.Length >= 3) { destination[0] = mask; if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && - EncodeStringLiteral(value, valueEncoding: null, destination.Slice(1 + nameLength), out int valueLength)) + EncodeStringLiteral(value, valueEncoding, destination.Slice(1 + nameLength), out int valueLength)) { bytesWritten = 1 + nameLength + valueLength; return true; @@ -643,7 +643,7 @@ public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int #endif while (true) { - if (EncodeLiteralHeaderFieldWithoutIndexing(index, value, span, out int length)) + if (EncodeLiteralHeaderFieldWithoutIndexing(index, value, valueEncoding: null, span, out int length)) { return span.Slice(0, length).ToArray(); } diff --git a/src/Shared/runtime/Http2/Hpack/HeaderField.cs b/src/Shared/runtime/Http2/Hpack/HeaderField.cs index 5127e6fb9538..aff43658c3ab 100644 --- a/src/Shared/runtime/Http2/Hpack/HeaderField.cs +++ b/src/Shared/runtime/Http2/Hpack/HeaderField.cs @@ -36,7 +36,7 @@ public override string ToString() { if (Name != null) { - return Encoding.ASCII.GetString(Name) + ": " + Encoding.ASCII.GetString(Value); + return Encoding.Latin1.GetString(Name) + ": " + Encoding.Latin1.GetString(Value); } else {