Просмотр исходного кода

Add Uri StringSyntaxAttribute syntaxes (#44570)

blouflashdb 3 лет назад
Родитель
Сommit
fa2fd21e24
38 измененных файлов с 199 добавлено и 95 удалено
  1. 6 5
      src/Components/Components/src/NavigationManager.cs
  2. 2 1
      src/Components/WebAssembly/Server/src/TargetPickerUi.cs
  3. 6 4
      src/Components/WebAssembly/WebAssembly.Authentication/src/NavigationManagerExtensions.cs
  4. 2 1
      src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.Log.cs
  5. 4 2
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs
  6. 2 1
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs
  7. 2 1
      src/Components/WebView/WebView/src/WebViewManager.cs
  8. 3 2
      src/DefaultBuilder/src/WebApplication.cs
  9. 3 3
      src/DefaultBuilder/src/WebHost.cs
  10. 2 2
      src/Hosting/Abstractions/src/HostingAbstractionsWebHostBuilderExtensions.cs
  11. 3 2
      src/Http/Http.Abstractions/src/HttpResponse.cs
  12. 2 1
      src/Http/Http.Extensions/src/UriHelper.cs
  13. 5 4
      src/Http/Http.Results/src/RedirectHttpResult.cs
  14. 3 2
      src/Http/Http.Results/src/Results.cs
  15. 3 2
      src/Http/Http.Results/src/TypedResults.cs
  16. 1 1
      src/Identity/Core/src/SignInManager.cs
  17. 4 3
      src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs
  18. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml.cs
  19. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml.cs
  20. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml.cs
  21. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs
  22. 4 3
      src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs
  23. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Login.cshtml.cs
  24. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V5/Account/LoginWith2fa.cshtml.cs
  25. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V5/Account/LoginWithRecoveryCode.cshtml.cs
  26. 3 2
      src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs
  27. 1 1
      src/Mvc/Mvc.Abstractions/src/IUrlHelper.cs
  28. 9 8
      src/Mvc/Mvc.Core/src/ControllerBase.cs
  29. 3 3
      src/Mvc/Mvc.Core/src/LocalRedirectResult.cs
  30. 3 3
      src/Mvc/Mvc.Core/src/RedirectResult.cs
  31. 1 1
      src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs
  32. 2 2
      src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
  33. 9 8
      src/Mvc/Mvc.RazorPages/src/PageBase.cs
  34. 9 8
      src/Mvc/Mvc.RazorPages/src/PageModel.cs
  35. 2 1
      src/Mvc/Mvc.TagHelpers/src/GlobbingUrlBuilder.cs
  36. 70 0
      src/Shared/CodeAnalysis/StringSyntaxAttribute.cs
  37. 5 4
      src/SignalR/clients/csharp/Client/src/HubConnectionBuilderHttpExtensions.cs
  38. 4 0
      src/SignalR/clients/csharp/Client/src/Microsoft.AspNetCore.SignalR.Client.csproj

+ 6 - 5
src/Components/Components/src/NavigationManager.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Components.Routing;
 
 namespace Microsoft.AspNetCore.Components;
@@ -103,7 +104,7 @@ public abstract class NavigationManager
     /// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
     /// (as returned by <see cref="BaseUri"/>).</param>
     /// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
-    public void NavigateTo(string uri, bool forceLoad) // This overload is for binary back-compat with < 6.0
+    public void NavigateTo([StringSyntax(StringSyntaxAttribute.Uri)] string uri, bool forceLoad) // This overload is for binary back-compat with < 6.0
         => NavigateTo(uri, forceLoad, replace: false);
 
     /// <summary>
@@ -113,7 +114,7 @@ public abstract class NavigationManager
     /// (as returned by <see cref="BaseUri"/>).</param>
     /// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
     /// <param name="replace">If true, replaces the current entry in the history stack. If false, appends the new entry to the history stack.</param>
-    public void NavigateTo(string uri, bool forceLoad = false, bool replace = false)
+    public void NavigateTo([StringSyntax(StringSyntaxAttribute.Uri)] string uri, bool forceLoad = false, bool replace = false)
     {
         AssertInitialized();
 
@@ -139,7 +140,7 @@ public abstract class NavigationManager
     /// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
     /// (as returned by <see cref="BaseUri"/>).</param>
     /// <param name="options">Provides additional <see cref="NavigationOptions"/>.</param>
-    public void NavigateTo(string uri, NavigationOptions options)
+    public void NavigateTo([StringSyntax(StringSyntaxAttribute.Uri)] string uri, NavigationOptions options)
     {
         AssertInitialized();
         NavigateToCore(uri, options);
@@ -155,7 +156,7 @@ public abstract class NavigationManager
     // already override this, so the framework needs to keep using it for the cases when only pre-6.0 options are used.
     // However, for anyone implementing a new NavigationManager post-6.0, we don't want them to have to override this
     // overload any more, so there's now a default implementation that calls the updated overload.
-    protected virtual void NavigateToCore(string uri, bool forceLoad)
+    protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)] string uri, bool forceLoad)
         => NavigateToCore(uri, new NavigationOptions { ForceLoad = forceLoad });
 
     /// <summary>
@@ -164,7 +165,7 @@ public abstract class NavigationManager
     /// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
     /// (as returned by <see cref="BaseUri"/>).</param>
     /// <param name="options">Provides additional <see cref="NavigationOptions"/>.</param>
-    protected virtual void NavigateToCore(string uri, NavigationOptions options) =>
+    protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)] string uri, NavigationOptions options) =>
         throw new NotImplementedException($"The type {GetType().FullName} does not support supplying {nameof(NavigationOptions)}. To add support, that type should override {nameof(NavigateToCore)}(string uri, {nameof(NavigationOptions)} options).");
 
     /// <summary>

+ 2 - 1
src/Components/WebAssembly/Server/src/TargetPickerUi.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
@@ -30,7 +31,7 @@ public class TargetPickerUi
     /// </summary>
     /// <param name="debugProxyUrl">The debug proxy url.</param>
     /// <param name="devToolsHost">The dev tools host.</param>
-    public TargetPickerUi(string debugProxyUrl, string devToolsHost)
+    public TargetPickerUi([StringSyntax(StringSyntaxAttribute.Uri)] string debugProxyUrl, string devToolsHost)
     {
         _debugProxyUrl = debugProxyUrl;
         _browserHost = devToolsHost;

+ 6 - 4
src/Components/WebAssembly/WebAssembly.Authentication/src/NavigationManagerExtensions.cs

@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
 
 /// <summary>
@@ -17,7 +19,7 @@ public static class NavigationManagerExtensions
     /// </remarks>
     /// <param name="manager">The <see cref="NavigationManager"/>.</param>
     /// <param name="logoutPath">The path to navigate to.</param>
-    public static void NavigateToLogout(this NavigationManager manager, string logoutPath) =>
+    public static void NavigateToLogout(this NavigationManager manager, [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string logoutPath) =>
         manager.NavigateToLogout(logoutPath, null);
 
     /// <summary>
@@ -30,7 +32,7 @@ public static class NavigationManagerExtensions
     /// <param name="manager">The <see cref="NavigationManager"/>.</param>
     /// <param name="logoutPath">The path to navigate too.</param>
     /// <param name="returnUrl">The url to redirect the user to after logging out.</param>
-    public static void NavigateToLogout(this NavigationManager manager, string logoutPath, string returnUrl)
+    public static void NavigateToLogout(this NavigationManager manager, [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string logoutPath, [StringSyntax(StringSyntaxAttribute.Uri)] string returnUrl)
     {
         manager.NavigateTo(logoutPath, new NavigationOptions
         {
@@ -52,7 +54,7 @@ public static class NavigationManagerExtensions
     /// <param name="manager">The <see cref="NavigationManager"/>.</param>
     /// <param name="loginPath">The path to the login url.</param>
     /// <param name="request">The <see cref="InteractiveRequestOptions"/> containing the authorization details.</param>
-    public static void NavigateToLogin(this NavigationManager manager, string loginPath, InteractiveRequestOptions request)
+    public static void NavigateToLogin(this NavigationManager manager, [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string loginPath, InteractiveRequestOptions request)
     {
         manager.NavigateTo(loginPath, new NavigationOptions
         {
@@ -69,7 +71,7 @@ public static class NavigationManagerExtensions
     /// </remarks>
     /// <param name="manager">The <see cref="NavigationManager"/>.</param>
     /// <param name="loginPath">The path to the login url.</param>
-    public static void NavigateToLogin(this NavigationManager manager, string loginPath)
+    public static void NavigateToLogin(this NavigationManager manager, [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string loginPath)
     {
         manager.NavigateToLogin(
             loginPath,

+ 2 - 1
src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.Log.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
@@ -19,7 +20,7 @@ public partial class RemoteAuthenticatorViewCore<TAuthenticationState> where TAu
         public static partial void LoginRequiresRedirect(ILogger logger);
 
         [LoggerMessage(4, LogLevel.Debug, "Navigating to {Url}.", EventName = nameof(NavigatingToUrl))]
-        public static partial void NavigatingToUrl(ILogger logger, string url);
+        public static partial void NavigatingToUrl(ILogger logger, [StringSyntax(StringSyntaxAttribute.Uri)] string url);
 
         [LoggerMessage(5, LogLevel.Debug, "Raising LoginCompleted event.", EventName = nameof(InvokingLoginCompletedCallback))]
         public static partial void InvokingLoginCompletedCallback(ILogger logger);

+ 4 - 2
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs

@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
 
 /// <summary>
@@ -17,7 +19,7 @@ public class AccessTokenResult
     /// <param name="token">The <see cref="AccessToken"/> in case it was successful.</param>
     /// <param name="redirectUrl">The redirect uri to go to for provisioning the token.</param>
     [Obsolete("Use the AccessTokenResult(AccessTokenResultStatus, AccessToken, string, InteractiveRequestOptions)")]
-    public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, string redirectUrl)
+    public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, [StringSyntax(StringSyntaxAttribute.Uri)] string redirectUrl)
     {
         Status = status;
         _token = token;
@@ -31,7 +33,7 @@ public class AccessTokenResult
     /// <param name="token">The <see cref="AccessToken"/> in case it was successful.</param>
     /// <param name="interactiveRequestUrl">The redirect uri to go to for provisioning the token with <see cref="NavigationManagerExtensions.NavigateToLogin(NavigationManager, string, InteractiveRequestOptions)"/>.</param>
     /// <param name="interactiveRequest">The <see cref="InteractiveRequestOptions"/> containing the parameters for the interactive authentication.</param>
-    public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, string interactiveRequestUrl, InteractiveRequestOptions interactiveRequest)
+    public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, [StringSyntax(StringSyntaxAttribute.Uri)] string interactiveRequestUrl, InteractiveRequestOptions interactiveRequest)
     {
         Status = status;
         _token = token;

+ 2 - 1
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Headers;
@@ -94,7 +95,7 @@ public class AuthorizationMessageHandler : DelegatingHandler, IDisposable
     public AuthorizationMessageHandler ConfigureHandler(
         IEnumerable<string> authorizedUrls,
         IEnumerable<string> scopes = null,
-        string returnUrl = null)
+        [StringSyntax(StringSyntaxAttribute.Uri)] string returnUrl = null)
     {
         if (_authorizedUris != null)
         {

+ 2 - 1
src/Components/WebView/WebView/src/WebViewManager.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using Microsoft.AspNetCore.Components.Web;
 using Microsoft.AspNetCore.StaticWebAssets;
@@ -62,7 +63,7 @@ public abstract class WebViewManager : IAsyncDisposable
     /// client-side routing.
     /// </summary>
     /// <param name="url">The URL, which may be absolute or relative to the application root.</param>
-    public void Navigate(string url)
+    public void Navigate([StringSyntax(StringSyntaxAttribute.Uri)] string url)
         => NavigateCore(new Uri(_appBaseUri, url));
 
     /// <summary>

+ 3 - 2
src/DefaultBuilder/src/WebApplication.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Hosting.Server.Features;
@@ -142,7 +143,7 @@ public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteB
     /// <returns>
     /// A <see cref="Task"/> that represents the entire runtime of the <see cref="WebApplication"/> from startup to shutdown.
     /// </returns>
-    public Task RunAsync(string? url = null)
+    public Task RunAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? url = null)
     {
         Listen(url);
         return HostingAbstractionsHostExtensions.RunAsync(this);
@@ -152,7 +153,7 @@ public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteB
     /// Runs an application and block the calling thread until host shutdown.
     /// </summary>
     /// <param name="url">The URL to listen to if the server hasn't been configured directly.</param>
-    public void Run(string? url = null)
+    public void Run([StringSyntax(StringSyntaxAttribute.Uri)] string? url = null)
     {
         Listen(url);
         HostingAbstractionsHostExtensions.Run(this);

+ 3 - 3
src/DefaultBuilder/src/WebHost.cs

@@ -38,7 +38,7 @@ public static class WebHost
     /// <param name="url">The URL the hosted application will listen on.</param>
     /// <param name="app">A delegate that handles requests to the application.</param>
     /// <returns>A started <see cref="IWebHost"/> that hosts the application.</returns>
-    public static IWebHost Start(string url, RequestDelegate app)
+    public static IWebHost Start([StringSyntax(StringSyntaxAttribute.Uri)] string url, RequestDelegate app)
     {
         var startupAssemblyName = app.GetMethodInfo().DeclaringType!.Assembly.GetName().Name;
         return StartWith(url: url, configureServices: null, app: appBuilder => appBuilder.Run(app), applicationName: startupAssemblyName);
@@ -60,7 +60,7 @@ public static class WebHost
     /// <param name="url">The URL the hosted application will listen on.</param>
     /// <param name="routeBuilder">A delegate that configures the router for handling requests to the application.</param>
     /// <returns>A started <see cref="IWebHost"/> that hosts the application.</returns>
-    public static IWebHost Start(string url, Action<IRouteBuilder> routeBuilder)
+    public static IWebHost Start([StringSyntax(StringSyntaxAttribute.Uri)] string url, Action<IRouteBuilder> routeBuilder)
     {
         var startupAssemblyName = routeBuilder.GetMethodInfo().DeclaringType!.Assembly.GetName().Name;
         return StartWith(url, services => services.AddRouting(), appBuilder => appBuilder.UseRouter(routeBuilder), applicationName: startupAssemblyName);
@@ -82,7 +82,7 @@ public static class WebHost
     /// <param name="url">The URL the hosted application will listen on.</param>
     /// <param name="app">The delegate that configures the <see cref="IApplicationBuilder"/>.</param>
     /// <returns>A started <see cref="IWebHost"/> that hosts the application.</returns>
-    public static IWebHost StartWith(string url, Action<IApplicationBuilder> app) =>
+    public static IWebHost StartWith([StringSyntax(StringSyntaxAttribute.Uri)] string url, Action<IApplicationBuilder> app) =>
         StartWith(url: url, configureServices: null, app: app, applicationName: null);
 
     private static IWebHost StartWith(string? url, Action<IServiceCollection>? configureServices, Action<IApplicationBuilder> app, string? applicationName)

+ 2 - 2
src/Hosting/Abstractions/src/HostingAbstractionsWebHostBuilderExtensions.cs

@@ -137,7 +137,7 @@ public static class HostingAbstractionsWebHostBuilderExtensions
     /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
     /// <param name="urls">The urls the hosted application will listen on.</param>
     /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
-    public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls)
+    public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] params string[] urls)
     {
         if (urls == null)
         {
@@ -187,7 +187,7 @@ public static class HostingAbstractionsWebHostBuilderExtensions
     /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to start.</param>
     /// <param name="urls">The urls the hosted application will listen on.</param>
     /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
-    public static IWebHost Start(this IWebHostBuilder hostBuilder, params string[] urls)
+    public static IWebHost Start(this IWebHostBuilder hostBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] params string[] urls)
     {
         var host = hostBuilder.UseUrls(urls).Build();
         host.StartAsync(CancellationToken.None).GetAwaiter().GetResult();

+ 3 - 2
src/Http/Http.Abstractions/src/HttpResponse.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.IO.Pipelines;
 
 namespace Microsoft.AspNetCore.Http;
@@ -126,7 +127,7 @@ public abstract class HttpResponse
     /// </summary>
     /// <param name="location">The URL to redirect the client to. This must be properly encoded for use in http headers
     /// where only ASCII characters are allowed.</param>
-    public virtual void Redirect(string location) => Redirect(location, permanent: false);
+    public virtual void Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string location) => Redirect(location, permanent: false);
 
     /// <summary>
     /// Returns a redirect response (HTTP 301 or HTTP 302) to the client.
@@ -134,7 +135,7 @@ public abstract class HttpResponse
     /// <param name="location">The URL to redirect the client to. This must be properly encoded for use in http headers
     /// where only ASCII characters are allowed.</param>
     /// <param name="permanent"><c>True</c> if the redirect is permanent (301), otherwise <c>false</c> (302).</param>
-    public abstract void Redirect(string location, bool permanent);
+    public abstract void Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string location, bool permanent);
 
     /// <summary>
     /// Starts the response by calling OnStarting() and making headers unmodifiable.

+ 2 - 1
src/Http/Http.Extensions/src/UriHelper.cs

@@ -2,6 +2,7 @@
 // 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;
 
@@ -105,7 +106,7 @@ public static class UriHelper
     /// <param name="query">The query, if any.</param>
     /// <param name="fragment">The fragment, if any.</param>
     public static void FromAbsolute(
-        string uri,
+        [StringSyntax(StringSyntaxAttribute.Uri)] string uri,
         out string scheme,
         out HostString host,
         out PathString path,

+ 5 - 4
src/Http/Http.Results/src/RedirectHttpResult.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -18,7 +19,7 @@ public sealed partial class RedirectHttpResult : IResult
     /// provided.
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
-    internal RedirectHttpResult(string url)
+    internal RedirectHttpResult([StringSyntax(StringSyntaxAttribute.Uri)] string url)
          : this(url, permanent: false)
     {
     }
@@ -29,7 +30,7 @@ public sealed partial class RedirectHttpResult : IResult
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
-    internal RedirectHttpResult(string url, bool permanent)
+    internal RedirectHttpResult([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent)
         : this(url, permanent, preserveMethod: false)
     {
     }
@@ -42,7 +43,7 @@ public sealed partial class RedirectHttpResult : IResult
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307)
     /// or permanent redirect (308) preserve the initial request method.</param>
-    internal RedirectHttpResult(string url, bool permanent, bool preserveMethod)
+    internal RedirectHttpResult([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent, bool preserveMethod)
         : this(url, acceptLocalUrlOnly: false, permanent, preserveMethod)
     { }
 
@@ -56,7 +57,7 @@ public sealed partial class RedirectHttpResult : IResult
     /// or permanent redirect (308) preserve the initial request method.</param>
     /// <param name="acceptLocalUrlOnly">If set to true, only local URLs are accepted
     /// and will throw an exception when the supplied URL is not considered local.</param>
-    internal RedirectHttpResult(string url, bool acceptLocalUrlOnly, bool permanent, bool preserveMethod)
+    internal RedirectHttpResult([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool acceptLocalUrlOnly, bool permanent, bool preserveMethod)
     {
         Url = url;
         Permanent = permanent;

+ 3 - 2
src/Http/Http.Results/src/Results.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.IO.Pipelines;
 using System.Security.Claims;
 using System.Text;
@@ -442,7 +443,7 @@ public static partial class Results
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
     /// <returns>The created <see cref="IResult"/> for the response.</returns>
-    public static IResult Redirect(string url, bool permanent = false, bool preserveMethod = false)
+    public static IResult Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent = false, bool preserveMethod = false)
         => TypedResults.Redirect(url, permanent, preserveMethod);
 
     /// <summary>
@@ -466,7 +467,7 @@ public static partial class Results
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
     /// <returns>The created <see cref="IResult"/> for the response.</returns>
-    public static IResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false)
+    public static IResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl, bool permanent = false, bool preserveMethod = false)
         => TypedResults.LocalRedirect(localUrl, permanent, preserveMethod);
 
     /// <summary>

+ 3 - 2
src/Http/Http.Results/src/TypedResults.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.IO.Pipelines;
 using System.Security.Claims;
 using System.Text;
@@ -553,7 +554,7 @@ public static class TypedResults
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
     /// <returns>The created <see cref="RedirectHttpResult"/> for the response.</returns>
-    public static RedirectHttpResult Redirect(string url, bool permanent = false, bool preserveMethod = false)
+    public static RedirectHttpResult Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent = false, bool preserveMethod = false)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -576,7 +577,7 @@ public static class TypedResults
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
     /// <returns>The created <see cref="RedirectHttpResult"/> for the response.</returns>
-    public static RedirectHttpResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false)
+    public static RedirectHttpResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl, bool permanent = false, bool preserveMethod = false)
     {
         if (string.IsNullOrEmpty(localUrl))
         {

+ 1 - 1
src/Identity/Core/src/SignInManager.cs

@@ -726,7 +726,7 @@ public class SignInManager<TUser> where TUser : class
     /// <param name="redirectUrl">The external login URL users should be redirected to during the login flow.</param>
     /// <param name="userId">The current user's identifier, which will be used to provide CSRF protection.</param>
     /// <returns>A configured <see cref="AuthenticationProperties"/>.</returns>
-    public virtual AuthenticationProperties ConfigureExternalAuthenticationProperties(string? provider, string? redirectUrl, string? userId = null)
+    public virtual AuthenticationProperties ConfigureExternalAuthenticationProperties(string? provider, [StringSyntax(StringSyntaxAttribute.Uri)] string? redirectUrl, string? userId = null)
     {
         var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
         properties.Items[LoginProviderKey] = provider;

+ 4 - 3
src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Encodings.Web;
@@ -73,19 +74,19 @@ public class ExternalLoginModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual IActionResult OnPost(string provider, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual IActionResult OnPost(string provider, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetCallbackAsync(string? returnUrl = null, string? remoteError = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetCallbackAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null, string? remoteError = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostConfirmationAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostConfirmationAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class ExternalLoginModel<TUser> : ExternalLoginModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
@@ -79,13 +80,13 @@ public abstract class LoginModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginModel<TUser> : LoginModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -64,13 +65,13 @@ public abstract class LoginWith2faModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetAsync(bool rememberMe, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetAsync(bool rememberMe, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(bool rememberMe, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync(bool rememberMe, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginWith2faModel<TUser> : LoginWith2faModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -51,13 +52,13 @@ public abstract class LoginWithRecoveryCodeModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginWithRecoveryCodeModel<TUser> : LoginWithRecoveryCodeModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Text.Encodings.Web;
@@ -81,13 +82,13 @@ public abstract class RegisterModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class RegisterModel<TUser> : RegisterModel where TUser : class

+ 4 - 3
src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Encodings.Web;
@@ -73,19 +74,19 @@ public class ExternalLoginModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual IActionResult OnPost(string provider, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual IActionResult OnPost(string provider, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetCallbackAsync(string? returnUrl = null, string? remoteError = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetCallbackAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null, string? remoteError = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostConfirmationAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostConfirmationAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class ExternalLoginModel<TUser> : ExternalLoginModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Login.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
@@ -79,13 +80,13 @@ public abstract class LoginModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginModel<TUser> : LoginModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V5/Account/LoginWith2fa.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -64,13 +65,13 @@ public abstract class LoginWith2faModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetAsync(bool rememberMe, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetAsync(bool rememberMe, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(bool rememberMe, string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync(bool rememberMe, [StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginWith2faModel<TUser> : LoginWith2faModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V5/Account/LoginWithRecoveryCode.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -51,13 +52,13 @@ public abstract class LoginWithRecoveryCodeModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class LoginWithRecoveryCodeModel<TUser> : LoginWithRecoveryCodeModel where TUser : class

+ 3 - 2
src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Text.Encodings.Web;
@@ -81,13 +82,13 @@ public abstract class RegisterModel : PageModel
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task OnGetAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task OnGetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 
     /// <summary>
     ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
     ///     directly from your code. This API may change or be removed in future releases.
     /// </summary>
-    public virtual Task<IActionResult> OnPostAsync(string? returnUrl = null) => throw new NotImplementedException();
+    public virtual Task<IActionResult> OnPostAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? returnUrl = null) => throw new NotImplementedException();
 }
 
 internal sealed class RegisterModel<TUser> : RegisterModel where TUser : class

+ 1 - 1
src/Mvc/Mvc.Abstractions/src/IUrlHelper.cs

@@ -70,7 +70,7 @@ public interface IUrlHelper
     /// </code>
     /// </para>
     /// </example>
-    bool IsLocalUrl([NotNullWhen(true)] string? url);
+    bool IsLocalUrl([NotNullWhen(true)][StringSyntax(StringSyntaxAttribute.Uri)] string? url);
 
     /// <summary>
     /// Generates a URL with an absolute path, which contains the route name, route values, protocol to use, host

+ 9 - 8
src/Mvc/Mvc.Core/src/ControllerBase.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
 using System.Security.Claims;
 using System.Text;
@@ -337,7 +338,7 @@ public abstract class ControllerBase
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual RedirectResult Redirect(string url)
+    public virtual RedirectResult Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -354,7 +355,7 @@ public abstract class ControllerBase
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual RedirectResult RedirectPermanent(string url)
+    public virtual RedirectResult RedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -372,7 +373,7 @@ public abstract class ControllerBase
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual RedirectResult RedirectPreserveMethod(string url)
+    public virtual RedirectResult RedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -390,7 +391,7 @@ public abstract class ControllerBase
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual RedirectResult RedirectPermanentPreserveMethod(string url)
+    public virtual RedirectResult RedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -407,7 +408,7 @@ public abstract class ControllerBase
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual LocalRedirectResult LocalRedirect(string localUrl)
+    public virtual LocalRedirectResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -424,7 +425,7 @@ public abstract class ControllerBase
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual LocalRedirectResult LocalRedirectPermanent(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -442,7 +443,7 @@ public abstract class ControllerBase
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual LocalRedirectResult LocalRedirectPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -460,7 +461,7 @@ public abstract class ControllerBase
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
     [NonAction]
-    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {

+ 3 - 3
src/Mvc/Mvc.Core/src/LocalRedirectResult.cs

@@ -21,7 +21,7 @@ public class LocalRedirectResult : ActionResult
     /// provided.
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
-    public LocalRedirectResult(string localUrl)
+    public LocalRedirectResult([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
          : this(localUrl, permanent: false)
     {
     }
@@ -32,7 +32,7 @@ public class LocalRedirectResult : ActionResult
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
-    public LocalRedirectResult(string localUrl, bool permanent)
+    public LocalRedirectResult([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl, bool permanent)
         : this(localUrl, permanent, preserveMethod: false)
     {
     }
@@ -44,7 +44,7 @@ public class LocalRedirectResult : ActionResult
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request's method.</param>
-    public LocalRedirectResult(string localUrl, bool permanent, bool preserveMethod)
+    public LocalRedirectResult([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl, bool permanent, bool preserveMethod)
     {
         if (string.IsNullOrEmpty(localUrl))
         {

+ 3 - 3
src/Mvc/Mvc.Core/src/RedirectResult.cs

@@ -22,7 +22,7 @@ public class RedirectResult : ActionResult, IKeepTempDataResult
     /// provided.
     /// </summary>
     /// <param name="url">The local URL to redirect to.</param>
-    public RedirectResult(string url)
+    public RedirectResult([StringSyntax(StringSyntaxAttribute.Uri)] string url)
         : this(url, permanent: false)
     {
         if (url == null)
@@ -37,7 +37,7 @@ public class RedirectResult : ActionResult, IKeepTempDataResult
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
-    public RedirectResult(string url, bool permanent)
+    public RedirectResult([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent)
         : this(url, permanent, preserveMethod: false)
     {
     }
@@ -49,7 +49,7 @@ public class RedirectResult : ActionResult, IKeepTempDataResult
     /// <param name="url">The URL to redirect to.</param>
     /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
     /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
-    public RedirectResult(string url, bool permanent, bool preserveMethod)
+    public RedirectResult([StringSyntax(StringSyntaxAttribute.Uri)] string url, bool permanent, bool preserveMethod)
     {
         if (url == null)
         {

+ 1 - 1
src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs

@@ -47,7 +47,7 @@ public abstract class UrlHelperBase : IUrlHelper
     public ActionContext ActionContext { get; }
 
     /// <inheritdoc />
-    public virtual bool IsLocalUrl([NotNullWhen(true)] string? url) => CheckIsLocalUrl(url);
+    public virtual bool IsLocalUrl([NotNullWhen(true)][StringSyntax(StringSyntaxAttribute.Uri)] string? url) => CheckIsLocalUrl(url);
 
     /// <inheritdoc />
     [return: NotNullIfNotNull("contentPath")]

+ 2 - 2
src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs

@@ -212,7 +212,7 @@ public class UrlResolutionTagHelper : TagHelper
     /// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
     /// <paramref name="url"/> could not be resolved.</param>
     /// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
-    protected bool TryResolveUrl(string url, out string? resolvedUrl)
+    protected bool TryResolveUrl([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string url, out string? resolvedUrl)
     {
         resolvedUrl = null;
         var start = FindRelativeStart(url);
@@ -238,7 +238,7 @@ public class UrlResolutionTagHelper : TagHelper
     /// not be resolved.
     /// </param>
     /// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
-    protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
+    protected bool TryResolveUrl([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
     {
         resolvedUrl = null;
         var start = FindRelativeStart(url);

+ 9 - 8
src/Mvc/Mvc.RazorPages/src/PageBase.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
 using System.Security.Claims;
 using System.Text;
@@ -408,7 +409,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirect(string localUrl)
+    public virtual LocalRedirectResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -424,7 +425,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPermanent(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -441,7 +442,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -458,7 +459,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -497,7 +498,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult Redirect(string url)
+    public virtual RedirectResult Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -513,7 +514,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPermanent(string url)
+    public virtual RedirectResult RedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -530,7 +531,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPreserveMethod(string url)
+    public virtual RedirectResult RedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -547,7 +548,7 @@ public abstract class PageBase : RazorPageBase
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPermanentPreserveMethod(string url)
+    public virtual RedirectResult RedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {

+ 9 - 8
src/Mvc/Mvc.RazorPages/src/PageModel.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
 using System.Security.Claims;
 using System.Text;
@@ -805,7 +806,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirect(string localUrl)
+    public virtual LocalRedirectResult LocalRedirect([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -821,7 +822,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPermanent(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -838,7 +839,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -855,7 +856,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="localUrl">The local URL to redirect to.</param>
     /// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
-    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod(string localUrl)
+    public virtual LocalRedirectResult LocalRedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Relative)] string localUrl)
     {
         if (string.IsNullOrEmpty(localUrl))
         {
@@ -916,7 +917,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    protected internal RedirectResult Redirect(string url)
+    protected internal RedirectResult Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -932,7 +933,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPermanent(string url)
+    public virtual RedirectResult RedirectPermanent([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -949,7 +950,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPreserveMethod(string url)
+    public virtual RedirectResult RedirectPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {
@@ -966,7 +967,7 @@ public abstract class PageModel : IAsyncPageFilter, IPageFilter
     /// </summary>
     /// <param name="url">The URL to redirect to.</param>
     /// <returns>The created <see cref="RedirectResult"/> for the response.</returns>
-    public virtual RedirectResult RedirectPermanentPreserveMethod(string url)
+    public virtual RedirectResult RedirectPermanentPreserveMethod([StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         if (string.IsNullOrEmpty(url))
         {

+ 2 - 1
src/Mvc/Mvc.TagHelpers/src/GlobbingUrlBuilder.cs

@@ -1,6 +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.Diagnostics.CodeAnalysis;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.FileProviders;
@@ -66,7 +67,7 @@ public class GlobbingUrlBuilder
     /// <param name="excludePattern">The file globbing exclude pattern.</param>
     /// <returns>The list of URLs</returns>
     public virtual IReadOnlyList<string> BuildUrlList(
-        string staticUrl,
+        [StringSyntax(StringSyntaxAttribute.Uri)] string staticUrl,
         string includePattern,
         string excludePattern)
     {

+ 70 - 0
src/Shared/CodeAnalysis/StringSyntaxAttribute.cs

@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NET5_0_OR_GREATER
+namespace System.Diagnostics.CodeAnalysis;
+
+/// <summary>Specifies the syntax used in a string.</summary>
+[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+internal sealed class StringSyntaxAttribute : Attribute
+{
+    /// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
+    /// <param name="syntax">The syntax identifier.</param>
+    public StringSyntaxAttribute(string syntax)
+    {
+        Syntax = syntax;
+        Arguments = Array.Empty<object?>();
+    }
+
+    /// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
+    /// <param name="syntax">The syntax identifier.</param>
+    /// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
+    public StringSyntaxAttribute(string syntax, params object?[] arguments)
+    {
+        Syntax = syntax;
+        Arguments = arguments;
+    }
+
+    /// <summary>Gets the identifier of the syntax used.</summary>
+    public string Syntax { get; }
+
+    /// <summary>Optional arguments associated with the specific syntax employed.</summary>
+    public object?[] Arguments { get; }
+
+    /// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary>
+    public const string CompositeFormat = nameof(CompositeFormat);
+
+    /// <summary>The syntax identifier for strings containing date format specifiers.</summary>
+    public const string DateOnlyFormat = nameof(DateOnlyFormat);
+
+    /// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
+    public const string DateTimeFormat = nameof(DateTimeFormat);
+
+    /// <summary>The syntax identifier for strings containing <see cref="Enum"/> format specifiers.</summary>
+    public const string EnumFormat = nameof(EnumFormat);
+
+    /// <summary>The syntax identifier for strings containing <see cref="Guid"/> format specifiers.</summary>
+    public const string GuidFormat = nameof(GuidFormat);
+
+    /// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
+    public const string Json = nameof(Json);
+
+    /// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
+    public const string NumericFormat = nameof(NumericFormat);
+
+    /// <summary>The syntax identifier for strings containing regular expressions.</summary>
+    public const string Regex = nameof(Regex);
+
+    /// <summary>The syntax identifier for strings containing time format specifiers.</summary>
+    public const string TimeOnlyFormat = nameof(TimeOnlyFormat);
+
+    /// <summary>The syntax identifier for strings containing <see cref="TimeSpan"/> format specifiers.</summary>
+    public const string TimeSpanFormat = nameof(TimeSpanFormat);
+
+    /// <summary>The syntax identifier for strings containing URIs.</summary>
+    public const string Uri = nameof(Uri);
+
+    /// <summary>The syntax identifier for strings containing XML.</summary>
+    public const string Xml = nameof(Xml);
+}
+#endif

+ 5 - 4
src/SignalR/clients/csharp/Client/src/HubConnectionBuilderHttpExtensions.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Net;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Connections;
@@ -23,7 +24,7 @@ public static class HubConnectionBuilderHttpExtensions
     /// <param name="hubConnectionBuilder">The <see cref="IHubConnectionBuilder" /> to configure.</param>
     /// <param name="url">The URL the <see cref="HttpConnection"/> will use.</param>
     /// <returns>The same instance of the <see cref="IHubConnectionBuilder"/> for chaining.</returns>
-    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, string url)
+    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] string url)
     {
         hubConnectionBuilder.WithUrlCore(new Uri(url), null, null);
         return hubConnectionBuilder;
@@ -36,7 +37,7 @@ public static class HubConnectionBuilderHttpExtensions
     /// <param name="url">The URL the <see cref="HttpConnection"/> will use.</param>
     /// <param name="configureHttpConnection">The delegate that configures the <see cref="HttpConnection"/>.</param>
     /// <returns>The same instance of the <see cref="IHubConnectionBuilder"/> for chaining.</returns>
-    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, string url, Action<HttpConnectionOptions> configureHttpConnection)
+    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] string url, Action<HttpConnectionOptions> configureHttpConnection)
     {
         hubConnectionBuilder.WithUrlCore(new Uri(url), null, configureHttpConnection);
         return hubConnectionBuilder;
@@ -49,7 +50,7 @@ public static class HubConnectionBuilderHttpExtensions
     /// <param name="url">The URL the <see cref="HttpConnection"/> will use.</param>
     /// <param name="transports">A bitmask combining one or more <see cref="HttpTransportType"/> values that specify what transports the client should use.</param>
     /// <returns>The same instance of the <see cref="IHubConnectionBuilder"/> for chaining.</returns>
-    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, string url, HttpTransportType transports)
+    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] string url, HttpTransportType transports)
     {
         hubConnectionBuilder.WithUrlCore(new Uri(url), transports, null);
         return hubConnectionBuilder;
@@ -63,7 +64,7 @@ public static class HubConnectionBuilderHttpExtensions
     /// <param name="transports">A bitmask combining one or more <see cref="HttpTransportType"/> values that specify what transports the client should use.</param>
     /// <param name="configureHttpConnection">The delegate that configures the <see cref="HttpConnection"/>.</param>
     /// <returns>The same instance of the <see cref="IHubConnectionBuilder"/> for chaining.</returns>
-    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, string url, HttpTransportType transports, Action<HttpConnectionOptions> configureHttpConnection)
+    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder hubConnectionBuilder, [StringSyntax(StringSyntaxAttribute.Uri)] string url, HttpTransportType transports, Action<HttpConnectionOptions> configureHttpConnection)
     {
         hubConnectionBuilder.WithUrlCore(new Uri(url), transports, configureHttpConnection);
         return hubConnectionBuilder;

+ 4 - 0
src/SignalR/clients/csharp/Client/src/Microsoft.AspNetCore.SignalR.Client.csproj

@@ -16,6 +16,10 @@
     <InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR.Client.FunctionalTests" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)CodeAnalysis\StringSyntaxAttribute.cs" Condition="'$(TargetFramework)' != '$(DefaultNetCoreTargetFramework)'" />
+  </ItemGroup>
+
   <ItemGroup>
     <SupportedPlatform Include="browser" />
   </ItemGroup>