From 65b683427f3ee6f1d77f3c88d58abc7206389385 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:49:22 +0100 Subject: [PATCH 1/8] Refactor UriHelper.cs to use a static lambda and UriComponents struct This commit includes a significant refactor of the `UriHelper.cs` file. Unused namespaces `System.Buffers` and `System.Runtime.CompilerServices` have been removed. The `InitializeAbsoluteUriStringSpanAction` delegate and `InitializeAbsoluteUriString` method, as well as the `CopyTextToBuffer` method, have been removed. A new struct `UriComponents` has been introduced to encapsulate the components of a URI: `Scheme`, `Host`, `PathBase`, `Path`, `Query`, and `Fragment`. The `string.Create` method call has been updated to use a new lambda function that leverages the `UriComponents` struct to copy each component of the URI into the buffer, adjusting the buffer slice as needed. This function also handles the case where both `PathBase` and `Path` components have a slash, removing the trailing slash from `PathBase` to avoid duplication. Finally, the `BuildAbsolute` method has been updated to use the new `UriComponents` struct and the updated `string.Create` call. --- src/Http/Http.Extensions/src/UriHelper.cs | 89 +++++++++++++---------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 6c98c4d4890b..7240b20611f1 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Text; namespace Microsoft.AspNetCore.Http.Extensions; @@ -17,7 +15,6 @@ public static class UriHelper private const char Hash = '#'; private const char QuestionMark = '?'; private static readonly string SchemeDelimiter = Uri.SchemeDelimiter; - private static readonly SpanAction InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString); /// /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. @@ -90,7 +87,41 @@ public static string BuildAbsolute( length--; } - return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction); + return string.Create( + length, + new UriComponents(scheme, hostText, pathBaseText, pathText, queryText, fragmentText), + static (buffer, uriComponents) => + { + uriComponents.Scheme.AsSpan().CopyTo(buffer); + buffer = buffer.Slice(uriComponents.Scheme.Length); + + Uri.SchemeDelimiter.CopyTo(buffer); + buffer = buffer.Slice(Uri.SchemeDelimiter.Length); + + uriComponents.Host.AsSpan().CopyTo(buffer); + buffer = buffer.Slice(uriComponents.Host.Length); + + var pathBaseSpan = uriComponents.PathBase.AsSpan(); + + if (uriComponents.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') + { + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + // Trim the last slash from pathBase. The total length was decremented before the call to string.Create. + pathBaseSpan = pathBaseSpan[..^1]; + } + + pathBaseSpan.CopyTo(buffer); + buffer = buffer.Slice(pathBaseSpan.Length); + + uriComponents.Path.CopyTo(buffer); + buffer = buffer.Slice(uriComponents.Path.Length); + + uriComponents.Query.CopyTo(buffer); + buffer = buffer.Slice(uriComponents.Query.Length); + + uriComponents.Fragment.CopyTo(buffer); + }); } /// @@ -224,45 +255,23 @@ public static string GetDisplayUrl(this HttpRequest request) .ToString(); } - /// - /// Copies the specified to the specified starting at the specified . - /// - /// The buffer to copy text to. - /// The buffer start index. - /// The text to copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int CopyTextToBuffer(Span buffer, int index, ReadOnlySpan text) - { - text.CopyTo(buffer.Slice(index, text.Length)); - return index + text.Length; - } - - /// - /// Initializes the URI for . - /// - /// The URI 's buffer. - /// The URI parts. - private static void InitializeAbsoluteUriString(Span buffer, (string scheme, string host, string pathBase, string path, string query, string fragment) uriParts) + private readonly struct UriComponents { - var index = 0; - - var pathBaseSpan = uriParts.pathBase.AsSpan(); + public readonly string Scheme; + public readonly string Host; + public readonly string PathBase; + public readonly string Path; + public readonly string Query; + public readonly string Fragment; - if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') + public UriComponents(string scheme, string host, string pathBase, string path, string query, string fragment) { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - // Trim the last slahs from pathBase. The total length was decremented before the call to string.Create. - pathBaseSpan = pathBaseSpan[..^1]; + Scheme = scheme; + Host = host; + PathBase = pathBase; + Path = path; + Query = query; + Fragment = fragment; } - - index = CopyTextToBuffer(buffer, index, uriParts.scheme.AsSpan()); - index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan()); - index = CopyTextToBuffer(buffer, index, uriParts.host.AsSpan()); - index = CopyTextToBuffer(buffer, index, pathBaseSpan); - index = CopyTextToBuffer(buffer, index, uriParts.path.AsSpan()); - index = CopyTextToBuffer(buffer, index, uriParts.query.AsSpan()); - _ = CopyTextToBuffer(buffer, index, uriParts.fragment.AsSpan()); } } From 67f8239f2b4282a246ea36188086dae47518c914 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:20:32 +0100 Subject: [PATCH 2/8] fixed "src/Http/Http.Extensions/src/UriHelper.cs(205,52): error CS0117" --- src/Http/Http.Extensions/src/UriHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 7240b20611f1..9986ba14092b 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -202,7 +202,7 @@ public static string Encode(Uri uri) } else { - return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + return uri.GetComponents(System.UriComponents.SerializationInfoString, UriFormat.UriEscaped); } } From 554aca9f8289d10858b85f0daab6e0a16a420a8a Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 9 Dec 2025 16:41:32 -0800 Subject: [PATCH 3/8] Update src/Http/Http.Extensions/src/UriHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Http/Http.Extensions/src/UriHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 07d6f94b360b..64a30eb7a4cf 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Text; namespace Microsoft.AspNetCore.Http.Extensions; From ec1a2371277a9e4000b9622ecc4a34438ed849fc Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 9 Dec 2025 16:53:48 -0800 Subject: [PATCH 4/8] Update src/Http/Http.Extensions/src/UriHelper.cs --- src/Http/Http.Extensions/src/UriHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 64a30eb7a4cf..0d79945970a6 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Http.Extensions; From 18f15ff7658bfb50b6bf978967160f8f1d68ed71 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:06:45 +0000 Subject: [PATCH 5/8] Update src/Http/Http.Extensions/src/UriHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Http/Http.Extensions/src/UriHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 0d79945970a6..9b53c782c9d0 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -113,13 +113,13 @@ public static string BuildAbsolute( pathBaseSpan.CopyTo(buffer); buffer = buffer.Slice(pathBaseSpan.Length); - uriComponents.Path.CopyTo(buffer); + uriComponents.Path.AsSpan().CopyTo(buffer); buffer = buffer.Slice(uriComponents.Path.Length); - uriComponents.Query.CopyTo(buffer); + uriComponents.Query.AsSpan().CopyTo(buffer); buffer = buffer.Slice(uriComponents.Query.Length); - uriComponents.Fragment.CopyTo(buffer); + uriComponents.Fragment.AsSpan().CopyTo(buffer); }); } From e5c8be35f38c9affe3a98232ef0185f441a7f912 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:07:00 +0000 Subject: [PATCH 6/8] Update src/Http/Http.Extensions/src/UriHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Http/Http.Extensions/src/UriHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 9b53c782c9d0..375df8f39e2e 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -94,7 +94,7 @@ public static string BuildAbsolute( uriComponents.Scheme.AsSpan().CopyTo(buffer); buffer = buffer.Slice(uriComponents.Scheme.Length); - Uri.SchemeDelimiter.CopyTo(buffer); + Uri.SchemeDelimiter.AsSpan().CopyTo(buffer); buffer = buffer.Slice(Uri.SchemeDelimiter.Length); uriComponents.Host.AsSpan().CopyTo(buffer); From dc1ffa07f7d8a0260de4e317d756f3c8861bcaa1 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:26:37 +0000 Subject: [PATCH 7/8] Change UriComponents to readonly ref struct Refactored UriComponents from a regular readonly struct to a readonly ref struct to enforce stack-only allocation and prevent boxing or heap storage. This enhances safety and performance for scenarios where UriComponents is used. --- src/Http/Http.Extensions/src/UriHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 375df8f39e2e..c7ae68c216ea 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -237,7 +237,7 @@ public static string GetDisplayUrl(this HttpRequest request) return string.Concat([request.Scheme, SchemeDelimiter, request.Host.Value, request.PathBase.Value, request.Path.Value, request.QueryString.Value]); } - private readonly struct UriComponents + private readonly ref struct UriComponents { public readonly string Scheme; public readonly string Host; From 6dd20ee9a710b73ff260565047c45263e8712ea8 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:08:09 +0000 Subject: [PATCH 8/8] Refactor UriHelper to use ReadOnlySpan for components Switch URI component handling from string to ReadOnlySpan to reduce allocations and improve performance. Update UriComponents struct and string.Create logic to operate directly on spans, and move pathBase trailing slash trimming to span operations. --- src/Http/Http.Extensions/src/UriHelper.cs | 91 ++++++++++------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index c7ae68c216ea..cd41d31001b5 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -54,28 +54,17 @@ public static string BuildAbsolute( { ArgumentNullException.ThrowIfNull(scheme); - var hostText = host.ToUriComponent(); - var pathBaseText = pathBase.ToUriComponent(); - var pathText = path.ToUriComponent(); - var queryText = query.ToUriComponent(); - var fragmentText = fragment.ToUriComponent(); + var hostText = host.ToUriComponent().AsSpan(); + var pathBaseText = pathBase.ToUriComponent().AsSpan(); + var pathText = path.ToUriComponent().AsSpan(); + var queryText = query.ToUriComponent().AsSpan(); + var fragmentText = fragment.ToUriComponent().AsSpan(); - // PERF: Calculate string length to allocate correct buffer size for string.Create. - var length = - scheme.Length + - Uri.SchemeDelimiter.Length + - hostText.Length + - pathBaseText.Length + - pathText.Length + - queryText.Length + - fragmentText.Length; - - if (string.IsNullOrEmpty(pathText)) + if (pathText.IsEmpty) { - if (string.IsNullOrEmpty(pathBaseText)) + if (pathBaseText.IsEmpty) { - pathText = "/"; - length++; + pathText = "/".AsSpan(); } } else if (pathBaseText.EndsWith('/')) @@ -83,43 +72,43 @@ public static string BuildAbsolute( // If the path string has a trailing slash and the other string has a leading slash, we need // to trim one of them. // Just decrement the total length, for now. - length--; + pathBaseText = pathBaseText[..^1]; } + // PERF: Calculate string length to allocate correct buffer size for string.Create. + var length = + scheme.Length + + Uri.SchemeDelimiter.Length + + hostText.Length + + pathBaseText.Length + + pathText.Length + + queryText.Length + + fragmentText.Length; + return string.Create( length, new UriComponents(scheme, hostText, pathBaseText, pathText, queryText, fragmentText), static (buffer, uriComponents) => { - uriComponents.Scheme.AsSpan().CopyTo(buffer); - buffer = buffer.Slice(uriComponents.Scheme.Length); - - Uri.SchemeDelimiter.AsSpan().CopyTo(buffer); - buffer = buffer.Slice(Uri.SchemeDelimiter.Length); + uriComponents.Scheme.CopyTo(buffer); + buffer = buffer[uriComponents.Scheme.Length..]; - uriComponents.Host.AsSpan().CopyTo(buffer); - buffer = buffer.Slice(uriComponents.Host.Length); + Uri.SchemeDelimiter.CopyTo(buffer); + buffer = buffer[Uri.SchemeDelimiter.Length..]; - var pathBaseSpan = uriComponents.PathBase.AsSpan(); + uriComponents.Host.CopyTo(buffer); + buffer = buffer[uriComponents.Host.Length..]; - if (uriComponents.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') - { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - // Trim the last slash from pathBase. The total length was decremented before the call to string.Create. - pathBaseSpan = pathBaseSpan[..^1]; - } + uriComponents.PathBase.CopyTo(buffer); + buffer = buffer[uriComponents.PathBase.Length..]; - pathBaseSpan.CopyTo(buffer); - buffer = buffer.Slice(pathBaseSpan.Length); + uriComponents.Path.CopyTo(buffer); + buffer = buffer[uriComponents.Path.Length..]; - uriComponents.Path.AsSpan().CopyTo(buffer); - buffer = buffer.Slice(uriComponents.Path.Length); + uriComponents.Query.CopyTo(buffer); + buffer = buffer[uriComponents.Query.Length..]; - uriComponents.Query.AsSpan().CopyTo(buffer); - buffer = buffer.Slice(uriComponents.Query.Length); - - uriComponents.Fragment.AsSpan().CopyTo(buffer); + uriComponents.Fragment.CopyTo(buffer); }); } @@ -239,14 +228,14 @@ public static string GetDisplayUrl(this HttpRequest request) private readonly ref struct UriComponents { - public readonly string Scheme; - public readonly string Host; - public readonly string PathBase; - public readonly string Path; - public readonly string Query; - public readonly string Fragment; - - public UriComponents(string scheme, string host, string pathBase, string path, string query, string fragment) + public readonly ReadOnlySpan Scheme; + public readonly ReadOnlySpan Host; + public readonly ReadOnlySpan PathBase; + public readonly ReadOnlySpan Path; + public readonly ReadOnlySpan Query; + public readonly ReadOnlySpan Fragment; + + public UriComponents(ReadOnlySpan scheme, ReadOnlySpan host, ReadOnlySpan pathBase, ReadOnlySpan path, ReadOnlySpan query, ReadOnlySpan fragment) { Scheme = scheme; Host = host;