diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index fc951704a85e..cd41d31001b5 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; namespace Microsoft.AspNetCore.Http.Extensions; @@ -16,7 +14,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. @@ -57,11 +54,26 @@ 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(); + + if (pathText.IsEmpty) + { + if (pathBaseText.IsEmpty) + { + pathText = "/".AsSpan(); + } + } + else if (pathBaseText.EndsWith('/')) + { + // 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. + pathBaseText = pathBaseText[..^1]; + } // PERF: Calculate string length to allocate correct buffer size for string.Create. var length = @@ -73,23 +85,31 @@ public static string BuildAbsolute( queryText.Length + fragmentText.Length; - if (string.IsNullOrEmpty(pathText)) - { - if (string.IsNullOrEmpty(pathBaseText)) + return string.Create( + length, + new UriComponents(scheme, hostText, pathBaseText, pathText, queryText, fragmentText), + static (buffer, uriComponents) => { - pathText = "/"; - length++; - } - } - else if (pathBaseText.EndsWith('/')) - { - // 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--; - } + uriComponents.Scheme.CopyTo(buffer); + buffer = buffer[uriComponents.Scheme.Length..]; + + Uri.SchemeDelimiter.CopyTo(buffer); + buffer = buffer[Uri.SchemeDelimiter.Length..]; + + uriComponents.Host.CopyTo(buffer); + buffer = buffer[uriComponents.Host.Length..]; + + uriComponents.PathBase.CopyTo(buffer); + buffer = buffer[uriComponents.PathBase.Length..]; + + uriComponents.Path.CopyTo(buffer); + buffer = buffer[uriComponents.Path.Length..]; - return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction); + uriComponents.Query.CopyTo(buffer); + buffer = buffer[uriComponents.Query.Length..]; + + uriComponents.Fragment.CopyTo(buffer); + }); } /// @@ -170,7 +190,7 @@ public static string Encode(Uri uri) } else { - return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + return uri.GetComponents(System.UriComponents.SerializationInfoString, UriFormat.UriEscaped); } } @@ -206,45 +226,23 @@ 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]); } - /// - /// 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 ref struct UriComponents { - var index = 0; - - var pathBaseSpan = uriParts.pathBase.AsSpan(); - - if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') + 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) { - // 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()); } }