diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index c16099a0a84e..bc4ff343a52a 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -783,19 +783,20 @@ stages: publishOnError: true includeForks: true - # Source build - - template: /eng/common/templates/job/source-build.yml - parameters: - platform: - name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' - buildScript: './eng/build.sh $(_PublishArgs) --no-build-repo-tasks' - skipPublishValidation: true - jobProperties: - timeoutInMinutes: 120 - variables: - # Log environment variables in binary logs to ease debugging - MSBUILDLOGALLENVIRONMENTVARIABLES: true + # Source build - skip until https://github.com/dotnet/source-build/issues/3111 is resolved + - ${{ if False }}: + - template: /eng/common/templates/job/source-build.yml + parameters: + platform: + name: 'Managed' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' + buildScript: './eng/build.sh $(_PublishArgs) --no-build-repo-tasks' + skipPublishValidation: true + jobProperties: + timeoutInMinutes: 120 + variables: + # Log environment variables in binary logs to ease debugging + MSBUILDLOGALLENVIRONMENTVARIABLES: true # Publish to the BAR and perform source indexing. Wait until everything else is done. - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: @@ -830,7 +831,6 @@ stages: - MacOS_Test - Linux_Test - Helix_x64 - - Source_Build_Managed pool: name: NetCore1ESPool-Internal demands: ImageOverride -equals 1es-windows-2019 @@ -869,7 +869,6 @@ stages: - MacOS_Test - Linux_Test - Helix_x64 - - Source_Build_Managed pool: name: NetCore1ESPool-Internal # Visual Studio Enterprise - no BuildTools agents exist internally and job must run on Windows diff --git a/.github/fabricbot.json b/.github/fabricbot.json index f6b29e5c7440..93458fab109e 100644 --- a/.github/fabricbot.json +++ b/.github/fabricbot.json @@ -2566,7 +2566,7 @@ } } ], - "taskName": "[Milestone Assignments] Assign `Current Milestone` to PRs merged to main" + "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to the `main` branch" } }, { @@ -2691,46 +2691,6 @@ ] } }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "merged" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/6.0" - } - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "taskName": "Assign milestone to PRs merged to \"release/6.0\"", - "actions": [ - { - "name": "addMilestone", - "parameters": { - "milestoneName": "6.0.2" - } - } - ] - } - }, { "taskType": "trigger", "capabilityId": "IssueResponder", @@ -3107,7 +3067,7 @@ { "name": "prTargetsBranch", "parameters": { - "branchName": "release/5.0" + "branchName": "release/7.0" } } ] @@ -3118,12 +3078,12 @@ "issues", "project_card" ], - "taskName": "Add release/5.0 targeting PRs to the servicing project", + "taskName": "Add release/7.0 targeting PRs to the servicing project", "actions": [ { "name": "addMilestone", "parameters": { - "milestoneName": "5.0.x" + "milestoneName": "7.0.x" } }, { @@ -3362,46 +3322,6 @@ ] } }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "merged" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/3.1" - } - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to release/3.1 branch", - "actions": [ - { - "name": "addMilestone", - "parameters": { - "milestoneName": "3.1.33" - } - } - ] - } - }, { "taskType": "trigger", "capabilityId": "IssueResponder", @@ -3433,10 +3353,14 @@ ], "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to release/6.0 branch", "actions": [ + { + "name": "removeMilestone", + "parameters": {} + }, { "name": "addMilestone", "parameters": { - "milestoneName": "6.0.13" + "milestoneName": "6.0.14" } } ] @@ -3473,10 +3397,14 @@ ], "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to release/7.0 branch", "actions": [ + { + "name": "removeMilestone", + "parameters": {} + }, { "name": "addMilestone", "parameters": { - "milestoneName": "7.0.2" + "milestoneName": "7.0.3" } } ] diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 84b88d8fbb4d..663aca404a22 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -141,6 +141,7 @@ and are generated based on the last package release. + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7910f49e68be..1a313c24cc99 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -302,22 +302,26 @@ https://github.com/dotnet/runtime 1936b44855b8f30ea406f0b088b05839682bc20c - + https://github.com/dotnet/arcade - 1b04d6de502c4108ada6ea8e5ccefdc2ddc3ee7b + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 - + https://github.com/dotnet/arcade - 1b04d6de502c4108ada6ea8e5ccefdc2ddc3ee7b + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 - + https://github.com/dotnet/arcade - 1b04d6de502c4108ada6ea8e5ccefdc2ddc3ee7b + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 - + https://github.com/dotnet/arcade - 1b04d6de502c4108ada6ea8e5ccefdc2ddc3ee7b + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 + + + https://github.com/dotnet/arcade + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 diff --git a/eng/Versions.props b/eng/Versions.props index 14ba44a615eb..d4176dab7a80 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -135,8 +135,9 @@ 8.0.0-alpha.1.23068.4 8.0.0-alpha.1.23068.4 - 8.0.0-beta.23063.7 - 8.0.0-beta.23063.7 + 8.0.0-beta.23067.5 + 8.0.0-beta.23067.5 + 8.0.0-beta.23067.5 8.0.0-alpha.1.23062.2 diff --git a/global.json b/global.json index cbb736e5083d..48c04c363db1 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.100-alpha.1.23061.8" + "version": "8.0.100-alpha.1.23073.1" }, "tools": { - "dotnet": "8.0.100-alpha.1.23061.8", + "dotnet": "8.0.100-alpha.1.23073.1", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -27,7 +27,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23063.7", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23063.7" + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23067.5", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23067.5" } } diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 1c172a30f54c..0496e1e028a5 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -175,6 +175,19 @@ protected Task InvokeAsync(Action workItem) protected Task InvokeAsync(Func workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem); + /// + /// Treats the supplied as being thrown by this component. This will cause the + /// enclosing ErrorBoundary to transition into a failed state. If there is no enclosing ErrorBoundary, + /// it will be regarded as an exception from the enclosing renderer. + /// + /// This is useful if an exception occurs outside the component lifecycle methods, but you wish to treat it + /// the same as an exception from a component lifecycle method. + /// + /// The that will be dispatched to the renderer. + /// A that will be completed when the exception has finished dispatching. + protected Task DispatchExceptionAsync(Exception exception) + => _renderHandle.DispatchExceptionAsync(exception); + void IComponent.Attach(RenderHandle renderHandle) { // This implicitly means a ComponentBase can only be associated with a single diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index d71ee2d249fc..34aa8535011c 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! *REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri! Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri! \ No newline at end of file diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index 62edaab1d7a2..fdd61f32ef3a 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -65,6 +65,18 @@ public void Render(RenderFragment renderFragment) _renderer.AddToRenderQueue(_componentId, renderFragment); } + /// + /// Dispatches an to the . + /// + /// The that will be dispatched to the renderer. + /// A that will be completed when the exception has finished dispatching. + public Task DispatchExceptionAsync(Exception exception) + { + var renderer = _renderer; + var componentId = _componentId; + return Dispatcher.InvokeAsync(() => renderer!.HandleComponentException(exception, componentId)); + } + [DoesNotReturn] private static void ThrowNotInitialized() { diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index b0e8ad00e7b0..dd39f0936ac1 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -926,6 +926,9 @@ private void UpdateRenderTreeToMatchClientState(ulong eventHandlerId, EventField } } + internal void HandleComponentException(Exception exception, int componentId) + => HandleExceptionViaErrorBoundary(exception, GetRequiredComponentState(componentId)); + /// /// If the exception can be routed to an error boundary around , do so. /// Otherwise handle it as fatal. diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index a6efff3b8951..6816812c8713 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3612,6 +3612,22 @@ public async Task ExceptionsThrownAsynchronouslyDuringFirstRenderCanBeHandled() Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException()); } + [Fact] + public async Task ExceptionsDispatchedOffSyncContextCanBeHandledAsync() + { + // Arrange + var renderer = new TestRenderer { ShouldHandleExceptions = true }; + var component = new NestedAsyncComponent(); + var exception = new InvalidTimeZoneException("Error from outside the sync context."); + + // Act + renderer.AssignRootComponentId(component); + await component.ExternalExceptionDispatch(exception); + + // Assert + Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException()); + } + [Fact] public async Task ExceptionsThrownAsynchronouslyAfterFirstRenderCanBeHandled() { @@ -5611,6 +5627,20 @@ public enum EventType OnAfterRenderAsyncSync, OnAfterRenderAsyncAsync, } + + public Task ExternalExceptionDispatch(Exception exception) + { + var tcs = new TaskCompletionSource(); + Task.Run(async () => + { + // Inside Task.Run, we're outside the call stack or task chain of the lifecycle method, so + // DispatchExceptionAsync is needed to get an exception back into the component + await DispatchExceptionAsync(exception); + tcs.SetResult(); + }); + + return tcs.Task; + } } private class ComponentThatAwaitsTask : ComponentBase diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoDebugger.ts b/src/Components/Web.JS/src/Platform/Mono/MonoDebugger.ts index c5682e806357..2f1ddfbb1e28 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoDebugger.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoDebugger.ts @@ -15,7 +15,7 @@ let hasReferencedPdbs = false; let debugBuild = false; export function hasDebuggingEnabled(): boolean { - return (hasReferencedPdbs || debugBuild) && currentBrowserIsChromeOrEdge; + return (hasReferencedPdbs || debugBuild) && (currentBrowserIsChromeOrEdge || navigator.userAgent.includes('Firefox')); } export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader): void { @@ -33,6 +33,8 @@ export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader): if (evt.shiftKey && (evt.metaKey || evt.altKey) && evt.code === 'KeyD') { if (!debugBuild && !hasReferencedPdbs) { console.error('Cannot start debugging, because the application was not compiled with debugging enabled.'); + } else if (navigator.userAgent.includes('Firefox')) { + launchFirefoxDebugger(); } else if (!currentBrowserIsChromeOrEdge) { console.error('Currently, only Microsoft Edge (80+), Google Chrome, or Chromium, are supported for debugging.'); } else { @@ -42,6 +44,13 @@ export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader): }); } +async function launchFirefoxDebugger() { + const response = await fetch(`_framework/debug?url=${encodeURIComponent(location.href)}&isFirefox=true`); + if (response.status !== 200) { + console.warn(await response.text()); + } +} + function launchDebugger() { // The noopener flag is essential, because otherwise Chrome tracks the association with the // parent tab, and then when the parent tab pauses in the debugger, the child tab does so diff --git a/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs index 5ab6bf1f6ae8..912877ab9b19 100644 --- a/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs +++ b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs @@ -18,25 +18,27 @@ internal static class DebugProxyLauncher private static Task? LaunchedDebugProxyUrl; private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?.*)$", RegexOptions.None, TimeSpan.FromSeconds(10)); private static readonly Regex ApplicationStartedRegex = new Regex(@"^\s*Application started\. Press Ctrl\+C to shut down\.$", RegexOptions.None, TimeSpan.FromSeconds(10)); + private static readonly Regex NowListeningFirefoxRegex = new Regex(@"^\s*Debug proxy for firefox now listening on tcp://(?.*)\. And expecting firefox at port 6000\.$", RegexOptions.None, TimeSpan.FromSeconds(10)); private static readonly string[] MessageSuppressionPrefixes = new[] { "Hosting environment:", "Content root path:", "Now listening on:", "Application started. Press Ctrl+C to shut down.", + "Debug proxy for firefox now", }; - public static Task EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider, string devToolsHost) + public static Task EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider, string devToolsHost, bool isFirefox) { lock (LaunchLock) { - LaunchedDebugProxyUrl ??= LaunchAndGetUrl(serviceProvider, devToolsHost); + LaunchedDebugProxyUrl ??= LaunchAndGetUrl(serviceProvider, devToolsHost, isFirefox); return LaunchedDebugProxyUrl; } } - private static async Task LaunchAndGetUrl(IServiceProvider serviceProvider, string devToolsHost) + private static async Task LaunchAndGetUrl(IServiceProvider serviceProvider, string devToolsHost, bool isFirefox) { var tcs = new TaskCompletionSource(); @@ -48,7 +50,7 @@ private static async Task LaunchAndGetUrl(IServiceProvider serviceProvid var processStartInfo = new ProcessStartInfo { FileName = muxerPath, - Arguments = $"exec \"{executablePath}\" --OwnerPid {ownerPid} --DevToolsUrl {devToolsHost}", + Arguments = $"exec \"{executablePath}\" --OwnerPid {ownerPid} --DevToolsUrl {devToolsHost} --IsFirefoxDebugging {isFirefox} --FirefoxProxyPort 6001", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -63,7 +65,7 @@ private static async Task LaunchAndGetUrl(IServiceProvider serviceProvid else { PassThroughConsoleOutput(debugProxyProcess); - CompleteTaskWhenServerIsReady(debugProxyProcess, tcs); + CompleteTaskWhenServerIsReady(debugProxyProcess, isFirefox, tcs); new CancellationTokenSource(DebugProxyLaunchTimeout).Token.Register(() => { @@ -136,7 +138,7 @@ private static void PassThroughConsoleOutput(Process process) }; } - private static void CompleteTaskWhenServerIsReady(Process aspNetProcess, TaskCompletionSource taskCompletionSource) + private static void CompleteTaskWhenServerIsReady(Process aspNetProcess, bool isFirefox, TaskCompletionSource taskCompletionSource) { string? capturedUrl = null; var errorEncountered = false; @@ -169,7 +171,7 @@ void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs) return; } - if (ApplicationStartedRegex.IsMatch(eventArgs.Data)) + if (ApplicationStartedRegex.IsMatch(eventArgs.Data) && !isFirefox) { aspNetProcess.OutputDataReceived -= OnOutputDataReceived; aspNetProcess.ErrorDataReceived -= OnErrorDataReceived; @@ -185,6 +187,15 @@ void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs) } else { + var matchFirefox = NowListeningFirefoxRegex.Match(eventArgs.Data); + if (matchFirefox.Success && isFirefox) + { + aspNetProcess.OutputDataReceived -= OnOutputDataReceived; + aspNetProcess.ErrorDataReceived -= OnErrorDataReceived; + capturedUrl = matchFirefox.Groups["url"].Value; + taskCompletionSource.TrySetResult(capturedUrl); + return; + } var match = NowListeningRegex.Match(eventArgs.Data); if (match.Success) { diff --git a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..bbfd1c31db86 100644 --- a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Components.WebAssembly.Server.TargetPickerUi.DisplayFirefox(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! diff --git a/src/Components/WebAssembly/Server/src/TargetPickerUi.cs b/src/Components/WebAssembly/Server/src/TargetPickerUi.cs index 3ad56a023e2f..a6af3fdebca7 100644 --- a/src/Components/WebAssembly/Server/src/TargetPickerUi.cs +++ b/src/Components/WebAssembly/Server/src/TargetPickerUi.cs @@ -5,9 +5,12 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Sockets; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; +using System.Dynamic; namespace Microsoft.AspNetCore.Components.WebAssembly.Server; @@ -37,6 +40,221 @@ public TargetPickerUi([StringSyntax(StringSyntaxAttribute.Uri)] string debugProx _browserHost = devToolsHost; } + /// + /// Display the ui. + /// + /// The . + /// The . + public async Task DisplayFirefox(HttpContext context) + { + static async Task SendMessageToBrowser(NetworkStream toStream, ExpandoObject args, CancellationToken token) + { + var msg = JsonSerializer.Serialize(args); + var bytes = Encoding.UTF8.GetBytes(msg); + var bytesWithHeader = Encoding.UTF8.GetBytes($"{bytes.Length}:").Concat(bytes).ToArray(); + await toStream.WriteAsync(bytesWithHeader, token).AsTask(); + } +#pragma warning disable CA1835 + static async Task ReceiveMessageLoop(TcpClient browserDebugClientConnect, CancellationToken token) + { + var toStream = browserDebugClientConnect.GetStream(); + var bytesRead = 0; + var _lengthBuffer = new byte[10]; + while (bytesRead == 0 || Convert.ToChar(_lengthBuffer[bytesRead - 1]) != ':') + { + if (!browserDebugClientConnect.Connected) + { + return ""; + } + + if (bytesRead + 1 > _lengthBuffer.Length) + { + throw new IOException($"Protocol error: did not get the expected length preceding a message, " + + $"after reading {bytesRead} bytes. Instead got: {Encoding.UTF8.GetString(_lengthBuffer)}"); + } + + int readLen = await toStream.ReadAsync(_lengthBuffer, bytesRead, 1, token); + bytesRead += readLen; + } + string str = Encoding.UTF8.GetString(_lengthBuffer, 0, bytesRead - 1); + if (!int.TryParse(str, out int messageLen)) + { + return ""; + } + byte[] buffer = new byte[messageLen]; + bytesRead = await toStream.ReadAsync(buffer, 0, messageLen, token); + while (bytesRead != messageLen) + { + if (!browserDebugClientConnect.Connected) + { + return ""; + } + bytesRead += await toStream.ReadAsync(buffer, bytesRead, messageLen - bytesRead, token); + } + var messageReceived = Encoding.UTF8.GetString(buffer, 0, messageLen); + return messageReceived; + } + static async Task EvaluateOnBrowser(NetworkStream toStream, string? to, string text, CancellationToken token) + { + dynamic message = new ExpandoObject(); + dynamic options = new ExpandoObject(); + dynamic awaitObj = new ExpandoObject(); + awaitObj.@await = true; + options.eager = true; + options.mapped = awaitObj; + message.to = to; + message.type = "evaluateJSAsync"; + message.text = text; + message.options = options; + await SendMessageToBrowser(toStream, message, token); + } +#pragma warning restore CA1835 + + context.Response.ContentType = "text/html"; + var request = context.Request; + var targetApplicationUrl = request.Query["url"]; + var browserDebugClientConnect = new TcpClient(); + if (IPEndPoint.TryParse(_debugProxyUrl, out IPEndPoint? endpoint)) + { + try + { + await browserDebugClientConnect.ConnectAsync(endpoint.Address, 6000); + } + catch (Exception) + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync($@"WARNING: +Open about:config: +- enable devtools.debugger.remote-enabled +- enable devtools.chrome.enabled +- disable devtools.debugger.prompt-connection +Open firefox with remote debugging enabled on port 6000: +firefox --start-debugger-server 6000 -new-tab about:debugging"); + return; + } + var source = new CancellationTokenSource(); + var token = source.Token; + var toStream = browserDebugClientConnect.GetStream(); + dynamic messageListTabs = new ExpandoObject(); + messageListTabs.type = "listTabs"; + messageListTabs.to = "root"; + await SendMessageToBrowser(toStream, messageListTabs, token); + var tabToRedirect = -1; + var foundAboutDebugging = false; + string? consoleActorId = null; + string? toCmd = null; + while (browserDebugClientConnect.Connected) + { + var res = System.Text.Json.JsonDocument.Parse(await ReceiveMessageLoop(browserDebugClientConnect, token)).RootElement; + var hasTabs = res.TryGetProperty("tabs", out var tabs); + var hasType = res.TryGetProperty("type", out var type); + if (hasType && type.GetString()?.Equals("tabListChanged", StringComparison.Ordinal) == true) + { + await SendMessageToBrowser(toStream, messageListTabs, token); + } + else + { + if (hasTabs) + { + var tabsList = tabs.Deserialize(); + if (tabsList == null) + { + continue; + } + foreach (var tab in tabsList) + { + var hasUrl = tab.TryGetProperty("url", out var urlInTab); + var hasActor = tab.TryGetProperty("actor", out var actorInTab); + var hasBrowserId = tab.TryGetProperty("browserId", out var browserIdInTab); + if (string.IsNullOrEmpty(consoleActorId)) + { + if (hasUrl && urlInTab.GetString()?.StartsWith("about:debugging#", StringComparison.InvariantCultureIgnoreCase) == true) + { + foundAboutDebugging = true; + + toCmd = hasActor ? actorInTab.GetString() : ""; + if (tabToRedirect != -1) + { + break; + } + } + if (hasUrl && urlInTab.GetString()?.Equals(targetApplicationUrl, StringComparison.Ordinal) == true) + { + tabToRedirect = hasBrowserId ? browserIdInTab.GetInt32() : -1; + if (foundAboutDebugging) + { + break; + } + } + } + else if (hasUrl && urlInTab.GetString()?.StartsWith("about:devtools", StringComparison.InvariantCultureIgnoreCase) == true) + { + return; + } + } + if (!foundAboutDebugging) + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("WARNING: Open about:debugging tab before pressing Debugging Hotkey"); + return; + } + if (string.IsNullOrEmpty(consoleActorId)) + { + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); + } + } + } + if (!string.IsNullOrEmpty(consoleActorId)) + { + var hasInput = res.TryGetProperty("input", out var input); + if (hasInput && input.GetString()?.StartsWith("AboutDebugging.actions.addNetworkLocation(", StringComparison.InvariantCultureIgnoreCase) == true) + { + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); + } + if (hasInput && input.GetString()?.StartsWith("if (AboutDebugging.store.getState()", StringComparison.InvariantCultureIgnoreCase) == true) + { + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); + } + } + else + { + var hasTarget = res.TryGetProperty("target", out var target); + JsonElement consoleActor = new(); + var hasConsoleActor = hasTarget && target.TryGetProperty("consoleActor", out consoleActor); + var hasActor = res.TryGetProperty("actor", out var actor); + if (hasConsoleActor && !string.IsNullOrEmpty(consoleActor.GetString())) + { + consoleActorId = consoleActor.GetString(); + await EvaluateOnBrowser(toStream, consoleActorId, $"AboutDebugging.actions.addNetworkLocation(\"{_debugProxyUrl}\"); AboutDebugging.actions.connectRuntime(\"{_debugProxyUrl}\");", token); + } + else if (hasActor && !string.IsNullOrEmpty(actor.GetString())) + { + dynamic messageWatchTargets = new ExpandoObject(); + messageWatchTargets.type = "watchTargets"; + messageWatchTargets.targetType = "frame"; + messageWatchTargets.to = actor.GetString(); + await SendMessageToBrowser(toStream, messageWatchTargets, token); + dynamic messageWatchResources = new ExpandoObject(); + messageWatchResources.type = "watchResources"; + messageWatchResources.resourceTypes = new string[1] { "console-message" }; + messageWatchResources.to = actor.GetString(); + await SendMessageToBrowser(toStream, messageWatchResources, token); + } + else if (!string.IsNullOrEmpty(toCmd)) + { + dynamic messageGetWatcher = new ExpandoObject(); + messageGetWatcher.type = "getWatcher"; + messageGetWatcher.isServerTargetSwitchingEnabled = true; + messageGetWatcher.to = toCmd; + await SendMessageToBrowser(toStream, messageGetWatcher, token); + } + } + } + + } + return; + } + /// /// Display the ui. /// diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs index 2d83f89682a4..59b81f59eee8 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs @@ -31,8 +31,12 @@ public static void UseWebAssemblyDebugging(this IApplicationBuilder app) browserUrl = new Uri(browserParam); devToolsHost = $"http://{browserUrl.Host}:{browserUrl.Port}"; } - - var debugProxyBaseUrl = await DebugProxyLauncher.EnsureLaunchedAndGetUrl(context.RequestServices, devToolsHost); + var isFirefox = string.IsNullOrEmpty(queryParams.Get("isFirefox")) ? false : true; + if (isFirefox) + { + devToolsHost = "localhost:6000"; + } + var debugProxyBaseUrl = await DebugProxyLauncher.EnsureLaunchedAndGetUrl(context.RequestServices, devToolsHost, isFirefox); var requestPath = context.Request.Path.ToString(); if (requestPath == string.Empty) { @@ -43,7 +47,14 @@ public static void UseWebAssemblyDebugging(this IApplicationBuilder app) { case "/": var targetPickerUi = new TargetPickerUi(debugProxyBaseUrl, devToolsHost); - await targetPickerUi.Display(context); + if (isFirefox) + { + await targetPickerUi.DisplayFirefox(context); + } + else + { + await targetPickerUi.Display(context); + } break; case "/ws-proxy": context.Response.Redirect($"{debugProxyBaseUrl}{browserUrl!.PathAndQuery}"); diff --git a/src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs index ba62819484cd..9ff97acdb14c 100644 --- a/src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs +++ b/src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs @@ -35,6 +35,8 @@ protected override void InitializeAsyncCore() [InlineData("afterrender-sync")] [InlineData("afterrender-async")] [InlineData("while-rendering")] + [InlineData("dispatch-sync-exception")] + [InlineData("dispatch-async-exception")] public void CanHandleExceptions(string triggerId) { var container = Browser.Exists(By.Id("error-boundary-container")); diff --git a/src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor b/src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor index 3b87c1e7c54f..bcc4aec249e0 100644 --- a/src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor +++ b/src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor @@ -99,6 +99,14 @@ +
+

Dispatch exception to renderer

+

Use DispatchExceptionAsync to see if exceptions are correctly dispatched to the renderer.

+
+ + +
+ @code { private bool throwInOnParametersSet; private bool throwInOnParametersSetAsync; @@ -143,4 +151,15 @@ // Before it completes, dispose its enclosing error boundary disposalTestRemoveErrorBoundary = true; } + + async Task SyncExceptionDispatch() + { + await DispatchExceptionAsync(new InvalidTimeZoneException("Synchronous exception in SyncExceptionDispatch")); + } + + async Task AsyncExceptionDispatch() + { + await Task.Yield(); + await DispatchExceptionAsync(new InvalidTimeZoneException("Asynchronous exception in AsyncExceptionDispatch")); + } } diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..c4dde0ab6378 100644 --- a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt +++ b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static Microsoft.AspNetCore.Builder.WebApplication.CreateSlimBuilder() -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! +static Microsoft.AspNetCore.Builder.WebApplication.CreateSlimBuilder(Microsoft.AspNetCore.Builder.WebApplicationOptions! options) -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! +static Microsoft.AspNetCore.Builder.WebApplication.CreateSlimBuilder(string![]! args) -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! diff --git a/src/DefaultBuilder/src/WebApplication.cs b/src/DefaultBuilder/src/WebApplication.cs index 1a9db8c6df1e..6056b3ff4d02 100644 --- a/src/DefaultBuilder/src/WebApplication.cs +++ b/src/DefaultBuilder/src/WebApplication.cs @@ -98,6 +98,13 @@ public static WebApplication Create(string[]? args = null) => public static WebApplicationBuilder CreateBuilder() => new(new()); + /// + /// Initializes a new instance of the class with minimal defaults. + /// + /// The . + public static WebApplicationBuilder CreateSlimBuilder() => + new(new(), slim: true); + /// /// Initializes a new instance of the class with preconfigured defaults. /// @@ -106,6 +113,14 @@ public static WebApplicationBuilder CreateBuilder() => public static WebApplicationBuilder CreateBuilder(string[] args) => new(new() { Args = args }); + /// + /// Initializes a new instance of the class with minimal defaults. + /// + /// Command line arguments + /// The . + public static WebApplicationBuilder CreateSlimBuilder(string[] args) => + new(new() { Args = args }, slim: true); + /// /// Initializes a new instance of the class with preconfigured defaults. /// @@ -114,6 +129,14 @@ public static WebApplicationBuilder CreateBuilder(string[] args) => public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options) => new(options); + /// + /// Initializes a new instance of the class with minimal defaults. + /// + /// The to configure the . + /// The . + public static WebApplicationBuilder CreateSlimBuilder(WebApplicationOptions options) => + new(options, slim: true); + /// /// Start the application. /// diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index 3da1ac4e83a7..0f7bca59694c 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -86,6 +86,97 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action? configureDefaults = null) + { + Debug.Assert(slim, "should only be called with slim: true"); + + var configuration = new ConfigurationManager(); + + configuration.AddEnvironmentVariables(prefix: "ASPNETCORE_"); + + // add the default host configuration sources, so they are picked up by the HostApplicationBuilder constructor. + // These won't be added by HostApplicationBuilder since DisableDefaults = true. + configuration.AddEnvironmentVariables(prefix: "DOTNET_"); + if (options.Args is { Length: > 0 } args) + { + configuration.AddCommandLine(args); + } + + _hostApplicationBuilder = new HostApplicationBuilder(new HostApplicationBuilderSettings + { + DisableDefaults = true, + Args = options.Args, + ApplicationName = options.ApplicationName, + EnvironmentName = options.EnvironmentName, + ContentRootPath = options.ContentRootPath, + Configuration = configuration, + }); + + // configure the ServiceProviderOptions here since DisableDefaults = true means HostApplicationBuilder won't. + var serviceProviderFactory = GetServiceProviderFactory(_hostApplicationBuilder); + _hostApplicationBuilder.ConfigureContainer(serviceProviderFactory); + + // Set WebRootPath if necessary + if (options.WebRootPath is not null) + { + Configuration.AddInMemoryCollection(new[] + { + new KeyValuePair(WebHostDefaults.WebRootKey, options.WebRootPath), + }); + } + + // Run methods to configure web host defaults early to populate services + var bootstrapHostBuilder = new BootstrapHostBuilder(_hostApplicationBuilder); + + // This is for testing purposes + configureDefaults?.Invoke(bootstrapHostBuilder); + + bootstrapHostBuilder.ConfigureSlimWebHost( + webHostBuilder => + { + AspNetCore.WebHost.UseKestrel(webHostBuilder); + + webHostBuilder.Configure(ConfigureEmptyApplication); + + webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, _hostApplicationBuilder.Environment.ApplicationName ?? ""); + webHostBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, Configuration[WebHostDefaults.PreventHostingStartupKey]); + webHostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, Configuration[WebHostDefaults.HostingStartupAssembliesKey]); + webHostBuilder.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, Configuration[WebHostDefaults.HostingStartupExcludeAssembliesKey]); + }, + options => + { + // We've already applied "ASPNETCORE_" environment variables to hosting config + options.SuppressEnvironmentConfiguration = true; + }); + + // This applies the config from ConfigureWebHostDefaults + // Grab the GenericWebHostService ServiceDescriptor so we can append it after any user-added IHostedServices during Build(); + _genericWebHostServiceDescriptor = bootstrapHostBuilder.RunDefaultCallbacks(); + + // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder. Then + // grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection. + var webHostContext = (WebHostBuilderContext)bootstrapHostBuilder.Properties[typeof(WebHostBuilderContext)]; + Environment = webHostContext.HostingEnvironment; + + Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services); + WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services); + } + + private static DefaultServiceProviderFactory GetServiceProviderFactory(HostApplicationBuilder hostApplicationBuilder) + { + if (hostApplicationBuilder.Environment.IsDevelopment()) + { + return new DefaultServiceProviderFactory( + new ServiceProviderOptions + { + ValidateScopes = true, + ValidateOnBuild = true, + }); + } + + return new DefaultServiceProviderFactory(); + } + /// /// Provides information about the web hosting environment an application is running. /// @@ -133,6 +224,46 @@ public WebApplication Build() } private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app) + { + ConfigureApplicationCore( + context, + app, + processAuthMiddlewares: () => + { + Debug.Assert(_builtApplication is not null); + + // Process authorization and authentication middlewares independently to avoid + // registering middlewares for services that do not exist + var serviceProviderIsService = _builtApplication.Services.GetService(); + if (serviceProviderIsService?.IsService(typeof(IAuthenticationSchemeProvider)) is true) + { + // Don't add more than one instance of the middleware + if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) + { + // The Use invocations will set the property on the outer pipeline, + // but we want to set it on the inner pipeline as well. + _builtApplication.Properties[AuthenticationMiddlewareSetKey] = true; + app.UseAuthentication(); + } + } + + if (serviceProviderIsService?.IsService(typeof(IAuthorizationHandlerProvider)) is true) + { + if (!_builtApplication.Properties.ContainsKey(AuthorizationMiddlewareSetKey)) + { + _builtApplication.Properties[AuthorizationMiddlewareSetKey] = true; + app.UseAuthorization(); + } + } + }); + } + + private void ConfigureEmptyApplication(WebHostBuilderContext context, IApplicationBuilder app) + { + ConfigureApplicationCore(context, app, processAuthMiddlewares: null); + } + + private void ConfigureApplicationCore(WebHostBuilderContext context, IApplicationBuilder app, Action? processAuthMiddlewares) { Debug.Assert(_builtApplication is not null); @@ -173,29 +304,7 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui } } - // Process authorization and authentication middlewares independently to avoid - // registering middlewares for services that do not exist - var serviceProviderIsService = _builtApplication.Services.GetService(); - if (serviceProviderIsService?.IsService(typeof(IAuthenticationSchemeProvider)) is true) - { - // Don't add more than one instance of the middleware - if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) - { - // The Use invocations will set the property on the outer pipeline, - // but we want to set it on the inner pipeline as well. - _builtApplication.Properties[AuthenticationMiddlewareSetKey] = true; - app.UseAuthentication(); - } - } - - if (serviceProviderIsService?.IsService(typeof(IAuthorizationHandlerProvider)) is true) - { - if (!_builtApplication.Properties.ContainsKey(AuthorizationMiddlewareSetKey)) - { - _builtApplication.Properties[AuthorizationMiddlewareSetKey] = true; - app.UseAuthorization(); - } - } + processAuthMiddlewares?.Invoke(); // Wire the source pipeline to run in the destination pipeline app.Use(next => diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 156efd0e326e..4a49c3ef8e3c 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -222,6 +222,16 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration); } }); + + UseKestrel(builder); + + builder + .UseIIS() + .UseIISIntegration(); + } + + internal static void UseKestrel(IWebHostBuilder builder) + { builder.UseKestrel((builderContext, options) => { options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true); @@ -248,9 +258,7 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) services.AddTransient, ForwardedHeadersOptionsSetup>(); services.AddRouting(); - }) - .UseIIS() - .UseIISIntegration(); + }); } /// diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index ea386a85e0b2..104e13788f2c 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -36,20 +36,77 @@ namespace Microsoft.AspNetCore.Tests; public class WebApplicationTests { - [Fact] - public async Task WebApplicationBuilder_New() + public delegate WebApplicationBuilder CreateBuilderFunc(); + public delegate WebApplicationBuilder CreateBuilderArgsFunc(string[] args); + public delegate WebApplicationBuilder CreateBuilderOptionsFunc(WebApplicationOptions options); + public delegate WebApplicationBuilder WebApplicationBuilderConstructorFunc(WebApplicationOptions options, Action configureDefaults); + + private static WebApplicationBuilder CreateBuilder() => WebApplication.CreateBuilder(); + private static WebApplicationBuilder CreateSlimBuilder() => WebApplication.CreateSlimBuilder(); + + public static IEnumerable CreateBuilderFuncs + { + get + { + yield return new[] { (CreateBuilderFunc)CreateBuilder }; + yield return new[] { (CreateBuilderFunc)CreateSlimBuilder }; + } + } + + private static WebApplicationBuilder CreateBuilderArgs(string[] args) => WebApplication.CreateBuilder(args); + private static WebApplicationBuilder CreateSlimBuilderArgs(string[] args) => WebApplication.CreateSlimBuilder(args); + + public static IEnumerable CreateBuilderArgsFuncs + { + get + { + yield return new[] { (CreateBuilderArgsFunc)CreateBuilderArgs }; + yield return new[] { (CreateBuilderArgsFunc)CreateSlimBuilderArgs }; + } + } + + private static WebApplicationBuilder CreateBuilderOptions(WebApplicationOptions options) => WebApplication.CreateBuilder(options); + private static WebApplicationBuilder CreateSlimBuilderOptions(WebApplicationOptions options) => WebApplication.CreateSlimBuilder(options); + + public static IEnumerable CreateBuilderOptionsFuncs + { + get + { + yield return new[] { (CreateBuilderOptionsFunc)CreateBuilderOptions }; + yield return new[] { (CreateBuilderOptionsFunc)CreateSlimBuilderOptions }; + } + } + + private static WebApplicationBuilder WebApplicationBuilderConstructor(WebApplicationOptions options, Action configureDefaults) + => new WebApplicationBuilder(options, configureDefaults); + private static WebApplicationBuilder WebApplicationSlimBuilderConstructor(WebApplicationOptions options, Action configureDefaults) + => new WebApplicationBuilder(options, slim: true, configureDefaults); + + public static IEnumerable WebApplicationBuilderConstructorFuncs { - var builder = WebApplication.CreateBuilder(new string[] { "--urls", "http://localhost:5001" }); + get + { + yield return new[] { (WebApplicationBuilderConstructorFunc)WebApplicationBuilderConstructor }; + yield return new[] { (WebApplicationBuilderConstructorFunc)WebApplicationSlimBuilderConstructor }; + } + } + + [Theory] + [MemberData(nameof(CreateBuilderArgsFuncs))] + public async Task WebApplicationBuilder_New(CreateBuilderArgsFunc createBuilder) + { + var builder = createBuilder(new string[] { "--urls", "http://localhost:5001" }); await using var app = builder.Build(); var newApp = (app as IApplicationBuilder).New(); Assert.NotNull(newApp.ServerFeatures); } - [Fact] - public async Task WebApplicationBuilderConfiguration_IncludesCommandLineArguments() + [Theory] + [MemberData(nameof(CreateBuilderArgsFuncs))] + public async Task WebApplicationBuilderConfiguration_IncludesCommandLineArguments(CreateBuilderArgsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new string[] { "--urls", "http://localhost:5001" }); + var builder = createBuilder(new string[] { "--urls", "http://localhost:5001" }); Assert.Equal("http://localhost:5001", builder.Configuration["urls"]); var urls = new List(); @@ -68,10 +125,11 @@ public async Task WebApplicationBuilderConfiguration_IncludesCommandLineArgument Assert.Equal("http://localhost:5001", url); } - [Fact] - public async Task WebApplicationRunAsync_UsesDefaultUrls() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationRunAsync_UsesDefaultUrls(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var urls = new List(); var server = new MockAddressesServer(urls); builder.Services.AddSingleton(server); @@ -86,10 +144,11 @@ public async Task WebApplicationRunAsync_UsesDefaultUrls() Assert.Equal("https://localhost:5001", urls[1]); } - [Fact] - public async Task WebApplicationRunUrls_UpdatesIServerAddressesFeature() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationRunUrls_UpdatesIServerAddressesFeature(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var urls = new List(); var server = new MockAddressesServer(urls); builder.Services.AddSingleton(server); @@ -104,10 +163,11 @@ public async Task WebApplicationRunUrls_UpdatesIServerAddressesFeature() await runTask; } - [Fact] - public async Task WebApplicationUrls_UpdatesIServerAddressesFeature() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationUrls_UpdatesIServerAddressesFeature(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var urls = new List(); var server = new MockAddressesServer(urls); builder.Services.AddSingleton(server); @@ -123,10 +183,11 @@ public async Task WebApplicationUrls_UpdatesIServerAddressesFeature() Assert.Equal("https://localhost:5003", urls[1]); } - [Fact] - public async Task WebApplicationRunUrls_OverridesIServerAddressesFeature() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationRunUrls_OverridesIServerAddressesFeature(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var urls = new List(); var server = new MockAddressesServer(urls); builder.Services.AddSingleton(server); @@ -144,20 +205,22 @@ public async Task WebApplicationRunUrls_OverridesIServerAddressesFeature() await runTask; } - [Fact] - public async Task WebApplicationUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Services.AddSingleton(new MockAddressesServer()); await using var app = builder.Build(); Assert.Throws(() => app.Urls); } - [Fact] - public async Task HostedServicesRunBeforeTheServerStarts() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task HostedServicesRunBeforeTheServerStarts(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var startOrder = new List(); var server = new MockServer(startOrder); var hostedService = new HostedService(startOrder); @@ -215,42 +278,47 @@ public Task StopAsync(CancellationToken cancellationToken) } } - [Fact] - public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Services.AddSingleton(new MockAddressesServer()); await using var app = builder.Build(); await Assert.ThrowsAsync(() => app.RunAsync("http://localhost:5001")); } - [Fact] - public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfServerAddressesFeatureIsReadOnly() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfServerAddressesFeatureIsReadOnly(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Services.AddSingleton(new MockAddressesServer(new List().AsReadOnly())); await using var app = builder.Build(); await Assert.ThrowsAsync(() => app.RunAsync("http://localhost:5001")); } - [Fact] - public void WebApplicationBuilderHost_ThrowsWhenBuiltDirectly() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderHost_ThrowsWhenBuiltDirectly(CreateBuilderFunc createBuilder) { - Assert.Throws(() => ((IHostBuilder)WebApplication.CreateBuilder().Host).Build()); + Assert.Throws(() => ((IHostBuilder)createBuilder().Host).Build()); } - [Fact] - public void WebApplicationBuilderWebHost_ThrowsWhenBuiltDirectly() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderWebHost_ThrowsWhenBuiltDirectly(CreateBuilderFunc createBuilder) { - Assert.Throws(() => ((IWebHostBuilder)WebApplication.CreateBuilder().WebHost).Build()); + Assert.Throws(() => ((IWebHostBuilder)createBuilder().WebHost).Build()); } - [Fact] - public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModified() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModified(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var webRoot = Path.Combine(contentRoot, "wwwroot"); @@ -266,10 +334,11 @@ public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModifie Assert.Throws(() => builder.WebHost.UseContentRoot(contentRoot)); } - [Fact] - public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModifiedViaConfigureAppConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModifiedViaConfigureAppConfiguration(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var webRoot = Path.Combine(contentRoot, "wwwroot"); @@ -324,13 +393,14 @@ public void WebApplicationBuilderWebHostSettingsThatAffectTheHostCannotBeModifie })); } - [Fact] - public void SettingContentRootToSameCanonicalValueWorks() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public void SettingContentRootToSameCanonicalValueWorks(CreateBuilderOptionsFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(contentRoot); - var builder = WebApplication.CreateBuilder(new WebApplicationOptions + var builder = createBuilder(new WebApplicationOptions { ContentRootPath = contentRoot }); @@ -344,13 +414,21 @@ public void SettingContentRootToSameCanonicalValueWorks() builder.WebHost.UseContentRoot(contentRoot.ToLowerInvariant()); } + public static IEnumerable CanHandleVariousWebRootPathsData + { + get + { + foreach (string webRoot in new[] { "wwwroot2", "./wwwroot2", "./bar/../wwwroot2", "foo/../wwwroot2", "wwwroot2/." }) + { + yield return new object[] { webRoot, (CreateBuilderOptionsFunc)CreateBuilderOptions }; + yield return new object[] { webRoot, (CreateBuilderOptionsFunc)CreateSlimBuilderOptions }; + } + } + } + [Theory] - [InlineData("wwwroot2")] - [InlineData("./wwwroot2")] - [InlineData("./bar/../wwwroot2")] - [InlineData("foo/../wwwroot2")] - [InlineData("wwwroot2/.")] - public void WebApplicationBuilder_CanHandleVariousWebRootPaths(string webRoot) + [MemberData(nameof(CanHandleVariousWebRootPathsData))] + public void WebApplicationBuilder_CanHandleVariousWebRootPaths(string webRoot, CreateBuilderOptionsFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -364,7 +442,7 @@ public void WebApplicationBuilder_CanHandleVariousWebRootPaths(string webRoot) WebRootPath = "wwwroot2" }; - var builder = new WebApplicationBuilder(options); + var builder = createBuilder(options); Assert.Equal(contentRoot, builder.Environment.ContentRootPath); Assert.Equal(fullWebRootPath, builder.Environment.WebRootPath); @@ -377,8 +455,9 @@ public void WebApplicationBuilder_CanHandleVariousWebRootPaths(string webRoot) } } - [Fact] - public void WebApplicationBuilder_CanOverrideWithFullWebRootPaths() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public void WebApplicationBuilder_CanOverrideWithFullWebRootPaths(CreateBuilderOptionsFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -392,7 +471,7 @@ public void WebApplicationBuilder_CanOverrideWithFullWebRootPaths() ContentRootPath = contentRoot, }; - var builder = new WebApplicationBuilder(options); + var builder = createBuilder(options); Assert.Equal(contentRoot, builder.Environment.ContentRootPath); Assert.Equal(fullWebRootPath, builder.Environment.WebRootPath); @@ -405,13 +484,21 @@ public void WebApplicationBuilder_CanOverrideWithFullWebRootPaths() } } + public static IEnumerable CanHandleVariousWebRootPaths_OverrideDefaultPathData + { + get + { + foreach (string webRoot in new[] { "wwwroot", "./wwwroot", "./bar/../wwwroot", "foo/../wwwroot", "wwwroot/." }) + { + yield return new object[] { webRoot, (CreateBuilderOptionsFunc)CreateBuilderOptions }; + yield return new object[] { webRoot, (CreateBuilderOptionsFunc)CreateSlimBuilderOptions }; + } + } + } + [Theory] - [InlineData("wwwroot")] - [InlineData("./wwwroot")] - [InlineData("./bar/../wwwroot")] - [InlineData("foo/../wwwroot")] - [InlineData("wwwroot/.")] - public void WebApplicationBuilder_CanHandleVariousWebRootPaths_OverrideDefaultPath(string webRoot) + [MemberData(nameof(CanHandleVariousWebRootPaths_OverrideDefaultPathData))] + public void WebApplicationBuilder_CanHandleVariousWebRootPaths_OverrideDefaultPath(string webRoot, CreateBuilderOptionsFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -425,7 +512,7 @@ public void WebApplicationBuilder_CanHandleVariousWebRootPaths_OverrideDefaultPa ContentRootPath = contentRoot }; - var builder = new WebApplicationBuilder(options); + var builder = createBuilder(options); Assert.Equal(contentRoot, builder.Environment.ContentRootPath); Assert.Equal(fullWebRootPath, builder.Environment.WebRootPath); @@ -438,12 +525,24 @@ public void WebApplicationBuilder_CanHandleVariousWebRootPaths_OverrideDefaultPa } } + public static IEnumerable SettingContentRootToRelativePathData + { + get + { + // Empty behaves differently to null + foreach (string path in new[] { "", "." }) + { + yield return new object[] { path, (CreateBuilderOptionsFunc)CreateBuilderOptions }; + yield return new object[] { path, (CreateBuilderOptionsFunc)CreateSlimBuilderOptions }; + } + } + } + [Theory] - [InlineData("")] // Empty behaves differently to null - [InlineData(".")] - public void SettingContentRootToRelativePathUsesAppContextBaseDirectoryAsPathBase(string path) + [MemberData(nameof(SettingContentRootToRelativePathData))] + public void SettingContentRootToRelativePathUsesAppContextBaseDirectoryAsPathBase(string path, CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions + var builder = createBuilder(new WebApplicationOptions { ContentRootPath = path }); @@ -462,8 +561,9 @@ static string NormalizePath(string unnormalizedPath) => Path.TrimEndingDirectorySeparator(Path.GetFullPath(unnormalizedPath)); } - [Fact] - public void WebApplicationBuilderSettingInvalidApplicationDoesNotThrowWhenAssemblyLoadForUserSecretsFail() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public void WebApplicationBuilderSettingInvalidApplicationDoesNotThrowWhenAssemblyLoadForUserSecretsFail(CreateBuilderOptionsFunc createBuilder) { var options = new WebApplicationOptions { @@ -472,14 +572,15 @@ public void WebApplicationBuilderSettingInvalidApplicationDoesNotThrowWhenAssemb }; // Use secrets fails to load an invalid assembly name but does not throw - var webApplication = WebApplication.CreateBuilder(options).Build(); + var webApplication = createBuilder(options).Build(); Assert.Equal(nameof(WebApplicationTests), webApplication.Environment.ApplicationName); Assert.Equal(Environments.Development, webApplication.Environment.EnvironmentName); } - [Fact] - public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOptions() + [Theory] + [MemberData(nameof(WebApplicationBuilderConstructorFuncs))] + public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOptions(WebApplicationBuilderConstructorFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -497,7 +598,7 @@ public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOpti WebRootPath = webRoot }; - var builder = new WebApplicationBuilder( + var builder = createBuilder( options, bootstrapBuilder => { @@ -520,8 +621,9 @@ public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOpti } } - [Fact] - public void WebApplicationBuilderWebApplicationOptionsPropertiesOverridesArgs() + [Theory] + [MemberData(nameof(WebApplicationBuilderConstructorFuncs))] + public void WebApplicationBuilderWebApplicationOptionsPropertiesOverridesArgs(WebApplicationBuilderConstructorFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -545,7 +647,7 @@ public void WebApplicationBuilderWebApplicationOptionsPropertiesOverridesArgs() WebRootPath = webRoot }; - var builder = new WebApplicationBuilder( + var builder = createBuilder( options, bootstrapBuilder => { @@ -568,8 +670,9 @@ public void WebApplicationBuilderWebApplicationOptionsPropertiesOverridesArgs() } } - [Fact] - public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOptionsArgs() + [Theory] + [MemberData(nameof(WebApplicationBuilderConstructorFuncs))] + public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOptionsArgs(WebApplicationBuilderConstructorFunc createBuilder) { var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(contentRoot); @@ -590,7 +693,7 @@ public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOpti } }; - var builder = new WebApplicationBuilder( + var builder = createBuilder( options, bootstrapBuilder => { @@ -613,12 +716,13 @@ public void WebApplicationBuilderCanConfigureHostSettingsUsingWebApplicationOpti } } - [Fact] - public void WebApplicationBuilderApplicationNameDefaultsToEntryAssembly() + [Theory] + [MemberData(nameof(WebApplicationBuilderConstructorFuncs))] + public void WebApplicationBuilderApplicationNameDefaultsToEntryAssembly(WebApplicationBuilderConstructorFunc createBuilder) { var assemblyName = Assembly.GetEntryAssembly().GetName().Name; - var builder = new WebApplicationBuilder( + var builder = createBuilder( new(), bootstrapBuilder => { @@ -649,8 +753,9 @@ public void WebApplicationBuilderApplicationNameDefaultsToEntryAssembly() Assert.Equal(assemblyName, webHostEnv.ApplicationName); } - [Fact] - public void WebApplicationBuilderApplicationNameCanBeOverridden() + [Theory] + [MemberData(nameof(WebApplicationBuilderConstructorFuncs))] + public void WebApplicationBuilderApplicationNameCanBeOverridden(WebApplicationBuilderConstructorFunc createBuilder) { var assemblyName = typeof(WebApplicationTests).Assembly.GetName().Name; @@ -659,7 +764,7 @@ public void WebApplicationBuilderApplicationNameCanBeOverridden() ApplicationName = assemblyName }; - var builder = new WebApplicationBuilder( + var builder = createBuilder( options, bootstrapBuilder => { @@ -690,10 +795,11 @@ public void WebApplicationBuilderApplicationNameCanBeOverridden() Assert.Equal(assemblyName, webHostEnv.ApplicationName); } - [Fact] - public void WebApplicationBuilderCanFlowCommandLineConfigurationToApplication() + [Theory] + [MemberData(nameof(CreateBuilderArgsFuncs))] + public void WebApplicationBuilderCanFlowCommandLineConfigurationToApplication(CreateBuilderArgsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new[] { "--x=1", "--name=Larry", "--age=20", "--environment=Testing" }); + var builder = createBuilder(new[] { "--x=1", "--name=Larry", "--age=20", "--environment=Testing" }); Assert.Equal("1", builder.Configuration["x"]); Assert.Equal("Larry", builder.Configuration["name"]); @@ -723,10 +829,11 @@ public void WebApplicationBuilderCanFlowCommandLineConfigurationToApplication() Assert.Equal("Testing", app.Configuration["environment"]); } - [Fact] - public void WebApplicationBuilderHostBuilderSettingsThatAffectTheHostCannotBeModified() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderHostBuilderSettingsThatAffectTheHostCannotBeModified(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var contentRoot = Path.GetTempPath().ToString(); var envName = $"{nameof(WebApplicationTests)}_ENV"; @@ -743,10 +850,11 @@ public void WebApplicationBuilderHostBuilderSettingsThatAffectTheHostCannotBeMod Assert.Throws(() => builder.Host.UseContentRoot(contentRoot)); } - [Fact] - public void WebApplicationBuilderWebHostUseSettingCanBeReadByConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderWebHostUseSettingCanBeReadByConfiguration(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseSetting("A", "value"); builder.WebHost.UseSetting("B", "another"); @@ -763,8 +871,9 @@ public void WebApplicationBuilderWebHostUseSettingCanBeReadByConfiguration() Assert.Equal("another", builder.Configuration["B"]); } - [Fact] - public async Task WebApplicationCanObserveConfigurationChangesMadeInBuild() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationCanObserveConfigurationChangesMadeInBuild(CreateBuilderFunc createBuilder) { // This mimics what WebApplicationFactory does and runs configure // services callbacks @@ -802,7 +911,7 @@ public async Task WebApplicationCanObserveConfigurationChangesMadeInBuild() }); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); await using var app = builder.Build(); @@ -821,8 +930,9 @@ public async Task WebApplicationCanObserveConfigurationChangesMadeInBuild() Assert.Equal("F", builder.Configuration["F"]); } - [Fact] - public async Task WebApplicationCanObserveSourcesClearedInBuild() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationCanObserveSourcesClearedInBuild(CreateBuilderFunc createBuilder) { // This mimics what WebApplicationFactory does and runs configure // services callbacks @@ -847,7 +957,7 @@ public async Task WebApplicationCanObserveSourcesClearedInBuild() }); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Configuration.AddInMemoryCollection(new Dictionary() { @@ -864,8 +974,9 @@ public async Task WebApplicationCanObserveSourcesClearedInBuild() Assert.Same(builder.Configuration, app.Configuration); } - [Fact] - public async Task WebApplicationCanObserveSourcesClearedInConfiguratHostConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task WebApplicationCanObserveSourcesClearedInConfiguratHostConfiguration(CreateBuilderOptionsFunc createBuilder) { // This mimics what WebApplicationFactory does and runs configure // services callbacks @@ -885,7 +996,7 @@ public async Task WebApplicationCanObserveSourcesClearedInConfiguratHostConfigur }); }); - var builder = WebApplication.CreateBuilder(new WebApplicationOptions + var builder = createBuilder(new WebApplicationOptions { ApplicationName = "appName", EnvironmentName = "environmentName", @@ -906,8 +1017,9 @@ public async Task WebApplicationCanObserveSourcesClearedInConfiguratHostConfigur Assert.Same(builder.Configuration, app.Configuration); } - [Fact] - public async Task WebApplicationCanHandleStreamBackedConfigurationAddedInBuild() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationCanHandleStreamBackedConfigurationAddedInBuild(CreateBuilderFunc createBuilder) { static Stream CreateStreamFromString(string data) => new MemoryStream(Encoding.UTF8.GetBytes(data)); @@ -922,7 +1034,7 @@ public async Task WebApplicationCanHandleStreamBackedConfigurationAddedInBuild() hostBuilder.ConfigureAppConfiguration(config => config.AddJsonStream(jsonBStream)); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); await using var app = builder.Build(); Assert.Equal("A", app.Configuration["A"]); @@ -931,8 +1043,9 @@ public async Task WebApplicationCanHandleStreamBackedConfigurationAddedInBuild() Assert.Same(builder.Configuration, app.Configuration); } - [Fact] - public async Task WebApplicationDisposesConfigurationProvidersAddedInBuild() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationDisposesConfigurationProvidersAddedInBuild(CreateBuilderFunc createBuilder) { var hostConfigSource = new RandomConfigurationSource(); var appConfigSource = new RandomConfigurationSource(); @@ -945,7 +1058,7 @@ public async Task WebApplicationDisposesConfigurationProvidersAddedInBuild() hostBuilder.ConfigureAppConfiguration(config => config.Add(appConfigSource)); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); { await using var app = builder.Build(); @@ -966,8 +1079,9 @@ public async Task WebApplicationDisposesConfigurationProvidersAddedInBuild() Assert.Equal(1, appConfigSource.ProvidersDisposed); } - [Fact] - public async Task WebApplicationMakesOriginalConfigurationProvidersAddedInBuildAccessable() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationMakesOriginalConfigurationProvidersAddedInBuildAccessable(CreateBuilderFunc createBuilder) { // This mimics what WebApplicationFactory does and runs configure // services callbacks @@ -976,16 +1090,17 @@ public async Task WebApplicationMakesOriginalConfigurationProvidersAddedInBuildA hostBuilder.ConfigureAppConfiguration(config => config.Add(new RandomConfigurationSource())); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); await using var app = builder.Build(); Assert.Single(((IConfigurationRoot)app.Configuration).Providers.OfType()); } - [Fact] - public void WebApplicationBuilderHostProperties_IsCaseSensitive() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilderHostProperties_IsCaseSensitive(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Host.Properties["lowercase"] = nameof(WebApplicationTests); @@ -993,10 +1108,11 @@ public void WebApplicationBuilderHostProperties_IsCaseSensitive() Assert.False(builder.Host.Properties.ContainsKey("Lowercase")); } - [Fact] - public async Task WebApplicationConfiguration_HostFilterOptionsAreReloadable() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationConfiguration_HostFilterOptionsAreReloadable(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var host = builder.WebHost .ConfigureAppConfiguration(configBuilder => { @@ -1023,10 +1139,11 @@ public async Task WebApplicationConfiguration_HostFilterOptionsAreReloadable() Assert.Contains("NewHost", options.AllowedHosts); } - [Fact] - public void CanResolveIConfigurationBeforeBuildingApplication() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void CanResolveIConfigurationBeforeBuildingApplication(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var sp = builder.Services.BuildServiceProvider(); var config = sp.GetService(); @@ -1038,10 +1155,11 @@ public void CanResolveIConfigurationBeforeBuildingApplication() Assert.Same(app.Configuration, builder.Configuration); } - [Fact] - public void ManuallyAddingConfigurationAsServiceWorks() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ManuallyAddingConfigurationAsServiceWorks(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Services.AddSingleton(builder.Configuration); var sp = builder.Services.BuildServiceProvider(); @@ -1054,10 +1172,11 @@ public void ManuallyAddingConfigurationAsServiceWorks() Assert.Same(app.Configuration, builder.Configuration); } - [Fact] - public void AddingMemoryStreamBackedConfigurationWorks() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void AddingMemoryStreamBackedConfigurationWorks(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var jsonConfig = @"{ ""foo"": ""bar"" }"; using var ms = new MemoryStream(); @@ -1075,10 +1194,11 @@ public void AddingMemoryStreamBackedConfigurationWorks() Assert.Equal("bar", app.Configuration["foo"]); } - [Fact] - public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); builder.Configuration["FORWARDEDHEADERS_ENABLED"] = "true"; await using var app = builder.Build(); @@ -1105,10 +1225,11 @@ public void WebApplicationCreate_RegistersRouting() Assert.NotNull(linkGenerator); } - [Fact] - public void WebApplication_CanResolveDefaultServicesFromServiceCollection() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplication_CanResolveDefaultServicesFromServiceCollection(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); // Add the service collection to the service collection builder.Services.AddSingleton(builder.Services); @@ -1124,8 +1245,9 @@ public void WebApplication_CanResolveDefaultServicesFromServiceCollection() Assert.Equal(env0.ContentRootPath, env1.ContentRootPath); } - [Fact] - public async Task WebApplication_CanResolveServicesAddedAfterBuildFromServiceCollection() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplication_CanResolveServicesAddedAfterBuildFromServiceCollection(CreateBuilderFunc createBuilder) { // This mimics what WebApplicationFactory does and runs configure // services callbacks @@ -1137,7 +1259,7 @@ public async Task WebApplication_CanResolveServicesAddedAfterBuildFromServiceCol }); }); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); // Add the service collection to the service collection builder.Services.AddSingleton(builder.Services); @@ -1152,10 +1274,11 @@ public async Task WebApplication_CanResolveServicesAddedAfterBuildFromServiceCol Assert.IsType(service1); } - [Fact] - public async Task WebApplication_CanResolveIConfigurationFromServiceCollection() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplication_CanResolveIConfigurationFromServiceCollection(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Configuration.AddInMemoryCollection(new Dictionary { @@ -1178,10 +1301,11 @@ public async Task WebApplication_CanResolveIConfigurationFromServiceCollection() Assert.Equal("bar", app.Configuration["foo"]); } - [Fact] - public void WebApplication_CanResolveDefaultServicesFromServiceCollectionInCorrectOrder() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplication_CanResolveDefaultServicesFromServiceCollectionInCorrectOrder(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); // Add the service collection to the service collection builder.Services.AddSingleton(builder.Services); @@ -1205,10 +1329,11 @@ public void WebApplication_CanResolveDefaultServicesFromServiceCollectionInCorre Assert.Equal(hostLifetimes1.Length, hostLifetimes0.Length); } - [Fact] - public async Task WebApplication_CanCallUseRoutingWithoutUseEndpoints() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplication_CanCallUseRoutingWithoutUseEndpoints(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1243,10 +1368,11 @@ public async Task WebApplication_CanCallUseRoutingWithoutUseEndpoints() Assert.Equal("new", await oldResult.Content.ReadAsStringAsync()); } - [Fact] - public async Task WebApplication_CanCallUseEndpointsWithoutUseRoutingFails() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplication_CanCallUseEndpointsWithoutUseRoutingFails(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1272,11 +1398,12 @@ public void WebApplicationCreate_RegistersEventSourceLogger() args.Payload.OfType().Any(p => p.Contains(guid))); } - [Fact] - public void WebApplicationBuilder_CanClearDefaultLoggers() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilder_CanClearDefaultLoggers(CreateBuilderFunc createBuilder) { var listener = new TestEventListener(); - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Logging.ClearProviders(); var app = builder.Build(); @@ -1291,10 +1418,11 @@ public void WebApplicationBuilder_CanClearDefaultLoggers() args.Payload.OfType().Any(p => p.Contains(guid))); } - [Fact] - public async Task WebApplicationBuilder_StartupFilterCanAddTerminalMiddleware() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationBuilder_StartupFilterCanAddTerminalMiddleware(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddSingleton(); await using var app = builder.Build(); @@ -1312,10 +1440,11 @@ public async Task WebApplicationBuilder_StartupFilterCanAddTerminalMiddleware() Assert.Equal(418, (int)terminalResult.StatusCode); } - [Fact] - public async Task StartupFilter_WithUseRoutingWorks() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task StartupFilter_WithUseRoutingWorks(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddSingleton(); await using var app = builder.Build(); @@ -1338,10 +1467,11 @@ public async Task StartupFilter_WithUseRoutingWorks() Assert.Equal(203, ((int)response.StatusCode)); } - [Fact] - public async Task CanAddMiddlewareBeforeUseRouting() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task CanAddMiddlewareBeforeUseRouting(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1375,13 +1505,33 @@ public async Task CanAddMiddlewareBeforeUseRouting() Assert.Equal("One", chosenEndpoint); } - [Fact] - public async Task WebApplicationBuilder_OnlyAddsDefaultServicesOnce() + public static IEnumerable OnlyAddsDefaultServicesOnceData { - var builder = WebApplication.CreateBuilder(); + get + { + // The slim builder doesn't add logging services by default + yield return new object[] { (CreateBuilderFunc)CreateBuilder, true }; + yield return new object[] { (CreateBuilderFunc)CreateSlimBuilder, false }; + } + } + + [Theory] + [MemberData(nameof(OnlyAddsDefaultServicesOnceData))] + public async Task WebApplicationBuilder_OnlyAddsDefaultServicesOnce(CreateBuilderFunc createBuilder, bool hasLogging) + { + var builder = createBuilder(); // IWebHostEnvironment is added by ConfigureDefaults - Assert.Single(builder.Services.Where(descriptor => descriptor.ServiceType == typeof(IConfigureOptions))); + var loggingDescriptors = builder.Services.Where(descriptor => descriptor.ServiceType == typeof(IConfigureOptions)); + if (hasLogging) + { + Assert.Single(loggingDescriptors); + } + else + { + Assert.Empty(loggingDescriptors); + } + // IWebHostEnvironment is added by ConfigureWebHostDefaults Assert.Single(builder.Services.Where(descriptor => descriptor.ServiceType == typeof(IWebHostEnvironment))); Assert.Single(builder.Services.Where(descriptor => descriptor.ServiceType == typeof(IOptionsChangeTokenSource))); @@ -1389,17 +1539,27 @@ public async Task WebApplicationBuilder_OnlyAddsDefaultServicesOnce() await using var app = builder.Build(); - Assert.Single(app.Services.GetRequiredService>>()); + var loggingServices = app.Services.GetRequiredService>>(); + if (hasLogging) + { + Assert.Single(loggingServices); + } + else + { + Assert.Empty(loggingServices); + } + Assert.Single(app.Services.GetRequiredService>()); Assert.Single(app.Services.GetRequiredService>>()); Assert.Single(app.Services.GetRequiredService>()); } - [Fact] - public void WebApplicationBuilder_EnablesServiceScopeValidationByDefaultInDevelopment() + [Theory] + [MemberData(nameof(CreateBuilderArgsFuncs))] + public void WebApplicationBuilder_EnablesServiceScopeValidationByDefaultInDevelopment(CreateBuilderArgsFunc createBuilder) { // The environment cannot be reconfigured after the builder is created currently. - var builder = WebApplication.CreateBuilder(new[] { "--environment", "Development" }); + var builder = createBuilder(new[] { "--environment", "Development" }); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -1409,10 +1569,11 @@ public void WebApplicationBuilder_EnablesServiceScopeValidationByDefaultInDevelo Assert.ThrowsAny(() => builder.Build()); } - [Fact] - public async Task WebApplicationBuilder_ThrowsExceptionIfServicesAlreadyBuilt() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplicationBuilder_ThrowsExceptionIfServicesAlreadyBuilt(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); await using var app = builder.Build(); Assert.Throws(() => builder.Services.AddSingleton(new Service())); @@ -1423,10 +1584,11 @@ public async Task WebApplicationBuilder_ThrowsExceptionIfServicesAlreadyBuilt() Assert.Throws(() => builder.Services[0] = ServiceDescriptor.Singleton(new Service())); } - [Fact] - public void WebApplicationBuilder_ThrowsFromExtensionMethodsNotSupportedByHostAndWebHost() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void WebApplicationBuilder_ThrowsFromExtensionMethodsNotSupportedByHostAndWebHost(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var ex = Assert.Throws(() => builder.WebHost.Configure(app => { })); var ex1 = Assert.Throws(() => builder.WebHost.Configure((context, app) => { })); @@ -1442,17 +1604,20 @@ public void WebApplicationBuilder_ThrowsFromExtensionMethodsNotSupportedByHostAn var ex5 = Assert.Throws(() => builder.Host.ConfigureWebHost(webHostBuilder => { })); var ex6 = Assert.Throws(() => builder.Host.ConfigureWebHost(webHostBuilder => { }, options => { })); - var ex7 = Assert.Throws(() => builder.Host.ConfigureWebHostDefaults(webHostBuilder => { })); + var ex7 = Assert.Throws(() => builder.Host.ConfigureSlimWebHost(webHostBuilder => { }, options => { })); + var ex8 = Assert.Throws(() => builder.Host.ConfigureWebHostDefaults(webHostBuilder => { })); Assert.Equal("ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.", ex5.Message); Assert.Equal("ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.", ex6.Message); Assert.Equal("ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.", ex7.Message); + Assert.Equal("ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.", ex8.Message); } - [Fact] - public async Task EndpointDataSourceOnlyAddsOnce() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task EndpointDataSourceOnlyAddsOnce(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); await using var app = builder.Build(); app.UseRouting(); @@ -1474,10 +1639,11 @@ public async Task EndpointDataSourceOnlyAddsOnce() Assert.Equal("Three", ds.Endpoints[2].DisplayName); } - [Fact] - public async Task RoutesAddedToCorrectMatcher() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task RoutesAddedToCorrectMatcher(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1509,10 +1675,11 @@ public async Task RoutesAddedToCorrectMatcher() Assert.Equal("One", chosenRoute); } - [Fact] - public async Task WebApplication_CallsUseRoutingAndUseEndpoints() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task WebApplication_CallsUseRoutingAndUseEndpoints(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1535,10 +1702,11 @@ public async Task WebApplication_CallsUseRoutingAndUseEndpoints() Assert.Equal("One", chosenRoute); } - [Fact] - public async Task BranchingPipelineHasOwnRoutes() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task BranchingPipelineHasOwnRoutes(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1599,10 +1767,11 @@ public async Task BranchingPipelineHasOwnRoutes() Assert.Equal("Four", chosenRoute); } - [Fact] - public async Task PropertiesPreservedFromInnerApplication() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task PropertiesPreservedFromInnerApplication(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Services.AddSingleton(); await using var app = builder.Build(); @@ -1611,10 +1780,11 @@ public async Task PropertiesPreservedFromInnerApplication() app.Start(); } - [Fact] - public async Task DeveloperExceptionPageIsOnByDefaltInDevelopment() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task DeveloperExceptionPageIsOnByDefaltInDevelopment(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); + var builder = createBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1632,10 +1802,11 @@ public async Task DeveloperExceptionPageIsOnByDefaltInDevelopment() Assert.Contains("text/plain", response.Content.Headers.ContentType.MediaType); } - [Fact] - public async Task DeveloperExceptionPageDoesNotGetCaughtByStartupFilters() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task DeveloperExceptionPageDoesNotGetCaughtByStartupFilters(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); + var builder = createBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); builder.WebHost.UseTestServer(); builder.Services.AddSingleton(); await using var app = builder.Build(); @@ -1649,10 +1820,11 @@ public async Task DeveloperExceptionPageDoesNotGetCaughtByStartupFilters() Assert.Equal("BOOM Filter", ex.Message); } - [Fact] - public async Task DeveloperExceptionPageIsNotOnInProduction() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task DeveloperExceptionPageIsNotOnInProduction(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Production }); + var builder = createBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Production }); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1670,6 +1842,7 @@ public async Task DeveloperExceptionPageIsNotOnInProduction() [Fact] public async Task HostingStartupRunsWhenApplicationIsNotEntryPoint() { + // NOTE: CreateSlimBuilder doesn't support Startups var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ApplicationName = typeof(WebApplicationTests).Assembly.FullName }); await using var app = builder.Build(); @@ -1679,6 +1852,7 @@ public async Task HostingStartupRunsWhenApplicationIsNotEntryPoint() [Fact] public async Task HostingStartupRunsWhenApplicationIsNotEntryPointWithArgs() { + // NOTE: CreateSlimBuilder doesn't support Startups var builder = WebApplication.CreateBuilder(new[] { "--applicationName", typeof(WebApplicationTests).Assembly.FullName }); await using var app = builder.Build(); @@ -1693,16 +1867,18 @@ public async Task HostingStartupRunsWhenApplicationIsNotEntryPointApplicationNam Args = new[] { "--applicationName", typeof(WebApplication).Assembly.FullName }, ApplicationName = typeof(WebApplicationTests).Assembly.FullName, }; + // NOTE: CreateSlimBuilder doesn't support Startups var builder = WebApplication.CreateBuilder(options); await using var app = builder.Build(); Assert.Equal("value", app.Configuration["testhostingstartup:config"]); } - [Fact] - public async Task DeveloperExceptionPageWritesBadRequestDetailsToResponseByDefaltInDevelopment() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task DeveloperExceptionPageWritesBadRequestDetailsToResponseByDefaltInDevelopment(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); + var builder = createBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1723,10 +1899,11 @@ public async Task DeveloperExceptionPageWritesBadRequestDetailsToResponseByDefal Assert.Contains("notAnInt", responseBody); } - [Fact] - public async Task NoExceptionAreThrownForBadRequestsInProduction() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public async Task NoExceptionAreThrownForBadRequestsInProduction(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Production }); + var builder = createBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Production }); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1746,10 +1923,11 @@ public async Task NoExceptionAreThrownForBadRequestsInProduction() Assert.Equal(string.Empty, responseBody); } - [Fact] - public void PropertiesArePropagated() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void PropertiesArePropagated(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Host.Properties["hello"] = "world"; var callbacks = 0; @@ -1777,8 +1955,9 @@ public void PropertiesArePropagated() Assert.Equal(0b00000111, callbacks); } - [Fact] - public void EmptyAppConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void EmptyAppConfiguration(CreateBuilderFunc createBuilder) { var wwwroot = Path.Combine(AppContext.BaseDirectory, "wwwroot"); bool createdDirectory = false; @@ -1790,7 +1969,7 @@ public void EmptyAppConfiguration() try { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.ConfigureAppConfiguration((ctx, config) => { }); @@ -1807,10 +1986,11 @@ public void EmptyAppConfiguration() } } - [Fact] - public void HostConfigurationNotAffectedByConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void HostConfigurationNotAffectedByConfiguration(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var contentRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var envName = $"{nameof(WebApplicationTests)}_ENV"; @@ -1836,10 +2016,11 @@ public void HostConfigurationNotAffectedByConfiguration() Assert.NotEqual(contentRoot, hostEnv.ContentRootPath); } - [Fact] - public void ClearingConfigurationDoesNotAffectHostConfiguration() + [Theory] + [MemberData(nameof(CreateBuilderOptionsFuncs))] + public void ClearingConfigurationDoesNotAffectHostConfiguration(CreateBuilderOptionsFunc createBuilder) { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions + var builder = createBuilder(new WebApplicationOptions { ApplicationName = typeof(WebApplicationOptions).Assembly.FullName, EnvironmentName = Environments.Staging, @@ -1865,10 +2046,11 @@ public void ClearingConfigurationDoesNotAffectHostConfiguration() Assert.Equal(Path.GetTempPath(), hostEnv.ContentRootPath); } - [Fact] - public void ConfigurationGetDebugViewWorks() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationGetDebugViewWorks(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.Configuration.AddInMemoryCollection(new Dictionary { @@ -1881,10 +2063,11 @@ public void ConfigurationGetDebugViewWorks() Assert.Contains("foo=bar (MemoryConfigurationProvider)", ((IConfigurationRoot)app.Configuration).GetDebugView()); } - [Fact] - public void ConfigurationCanBeReloaded() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationCanBeReloaded(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); ((IConfigurationBuilder)builder.Configuration).Sources.Add(new RandomConfigurationSource()); @@ -1897,10 +2080,11 @@ public void ConfigurationCanBeReloaded() Assert.NotEqual(value0, value1); } - [Fact] - public void ConfigurationSourcesAreBuiltOnce() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationSourcesAreBuiltOnce(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var configSource = new RandomConfigurationSource(); ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); @@ -1910,10 +2094,11 @@ public void ConfigurationSourcesAreBuiltOnce() Assert.Equal(1, configSource.ProvidersBuilt); } - [Fact] - public void ConfigurationProvidersAreLoadedOnceAfterBuild() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationProvidersAreLoadedOnceAfterBuild(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var configSource = new RandomConfigurationSource(); ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); @@ -1923,10 +2108,11 @@ public void ConfigurationProvidersAreLoadedOnceAfterBuild() Assert.Equal(1, configSource.ProvidersLoaded); } - [Fact] - public void ConfigurationProvidersAreDisposedWithWebApplication() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationProvidersAreDisposedWithWebApplication(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); var configSource = new RandomConfigurationSource(); ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); @@ -1940,10 +2126,11 @@ public void ConfigurationProvidersAreDisposedWithWebApplication() Assert.Equal(1, configSource.ProvidersDisposed); } - [Fact] - public void ConfigurationProviderTypesArePreserved() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void ConfigurationProviderTypesArePreserved(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); ((IConfigurationBuilder)builder.Configuration).Sources.Add(new RandomConfigurationSource()); @@ -1952,10 +2139,11 @@ public void ConfigurationProviderTypesArePreserved() Assert.Single(((IConfigurationRoot)app.Configuration).Providers.OfType()); } - [Fact] - public async Task CanUseMiddleware() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public async Task CanUseMiddleware(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); @@ -1972,10 +2160,11 @@ public async Task CanUseMiddleware() Assert.Equal("Hello World", response); } - [Fact] - public void CanObserveDefaultServicesInServiceCollection() + [Theory] + [MemberData(nameof(CreateBuilderFuncs))] + public void CanObserveDefaultServicesInServiceCollection(CreateBuilderFunc createBuilder) { - var builder = WebApplication.CreateBuilder(); + var builder = createBuilder(); Assert.Contains(builder.Services, service => service.ServiceType == typeof(HostBuilderContext)); Assert.Contains(builder.Services, service => service.ServiceType == typeof(IHostApplicationLifetime)); @@ -1985,20 +2174,32 @@ public void CanObserveDefaultServicesInServiceCollection() Assert.Contains(builder.Services, service => service.ServiceType == typeof(ILogger<>)); } - [Fact] - public async Task RegisterAuthMiddlewaresCorrectly() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RegisterAuthMiddlewaresCorrectly(bool useSlimBuilder) { var helloEndpointCalled = false; var customMiddlewareExecuted = false; var username = "foobar"; - var builder = WebApplication.CreateBuilder(); + var builder = useSlimBuilder ? + WebApplication.CreateSlimBuilder() : + WebApplication.CreateBuilder(); + builder.Services.AddAuthorization(); builder.Services.AddAuthentication("testSchemeName") .AddScheme("testSchemeName", "testDisplayName", _ => { }); builder.WebHost.UseTestServer(); await using var app = builder.Build(); + if (useSlimBuilder) + { + // NOTE: CreateSlimBuilder doesn't support auto registration of auth middleware, so need to do it explicitly + app.UseAuthentication(); + app.UseAuthorization(); + } + app.Use(next => { return async context => @@ -2031,6 +2232,7 @@ public async Task RegisterAuthMiddlewaresCorrectly() [Fact] public async Task SupportsDisablingMiddlewareAutoRegistration() { + // NOTE: CreateSlimBuilder doesn't support auto registration of auth middleware var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthorization(); builder.Services.AddAuthentication("testSchemeName") diff --git a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml index 5e0a19c24b5d..053fd4d1b580 100644 --- a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml @@ -1,6 +1,6 @@  - + PKV004 net7.0 diff --git a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml index a5bc9f2a9b13..36fe412dc72d 100644 --- a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml @@ -1,6 +1,6 @@  - + PKV0001 net7.0 diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs index 2d83b04a2053..a498a341ac60 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs @@ -94,7 +94,12 @@ public void Visit(RoutePatternCatchAllParameterPartNode node) public void Visit(RoutePatternNameParameterPartNode node) { - AddClassification(node.ParameterNameToken, ClassificationTypeNames.ParameterName); + // Parameter name should look like a regular parameter. + // ClassificationTypeNames.ParameterName isn't working so temporarily reuse color from JSON syntax: + // https://github.com/dotnet/roslyn/blob/e1ee2f544a7a4f8d536278bed4e180c54919276e/src/Workspaces/Core/Portable/Classification/ClassificationTypeNames.cs#L181 + // + // TODO: Fix properly with https://github.com/dotnet/aspnetcore/issues/46207 + AddClassification(node.ParameterNameToken, "json - object"); } public void Visit(RoutePatternPolicyParameterPartNode node) diff --git a/src/Framework/AspNetCoreAnalyzers/test/Http/HeaderDictionaryAddTest.cs b/src/Framework/AspNetCoreAnalyzers/test/Http/HeaderDictionaryAddTest.cs index 5eaa129e66fe..13684257b8b8 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Http/HeaderDictionaryAddTest.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/Http/HeaderDictionaryAddTest.cs @@ -147,22 +147,25 @@ public static IEnumerable FixedWithAppendAddsUsingDirectiveTestData() }; } - [ConditionalTheory] - [OSSkipCondition(OperatingSystems.Linux)] - [OSSkipCondition(OperatingSystems.MacOSX)] + [Theory] [MemberData(nameof(FixedWithAppendAddsUsingDirectiveTestData))] public async Task IHeaderDictionary_WithAdd_FixedWithAppend_AddsUsingDirective(string source, string fixedSource) { + // Source is cloned on Windows with CRLF line endings, then the test is run by Helix in Windows/Linux/macOS. + // When Roslyn adds a new `using`, it gets added followed by Environment.NewLine. + // For Linux/macOS, the actual result is `\n`, however, the source is cloned on Windows with CRLF expectation. + // We replace all line endings with Environment.NewLine to avoid this. + // Arrange & Act & Assert await VerifyCS.VerifyCodeFixAsync( - source.TrimStart(), + source.TrimStart().ReplaceLineEndings(), new[] { new DiagnosticResult(DiagnosticDescriptors.DoNotUseIHeaderDictionaryAdd) .WithLocation(0) .WithMessage(Resources.Analyzer_HeaderDictionaryAdd_Message) }, - fixedSource.TrimStart(), + fixedSource.TrimStart().ReplaceLineEndings(), codeActionEquivalenceKey: AppendCodeActionEquivalenceKey); } diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs index c2e0244515b1..b36f3fba9949 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs @@ -152,4 +152,6 @@ public void TestAction() Regex.CharacterClass("one"), Regex.CharacterClass("]")); } + + private static FormattedClassification Parameter(string name) => new FormattedClassification(name, "json - object"); } diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs index 0116dd0c8410..d89ddfe9331c 100644 --- a/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs +++ b/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs @@ -18,10 +18,8 @@ namespace Microsoft.AspNetCore.Hosting; -internal sealed class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider +internal sealed class GenericWebHostBuilder : WebHostBuilderBase, ISupportsStartup { - private readonly IHostBuilder _builder; - private readonly IConfiguration _config; private object? _startupObject; private readonly object _startupKey = new object(); @@ -29,18 +27,8 @@ internal sealed class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, private HostingStartupWebHostBuilder? _hostingStartupWebHostBuilder; public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options) + : base(builder, options) { - _builder = builder; - var configBuilder = new ConfigurationBuilder() - .AddInMemoryCollection(); - - if (!options.SuppressEnvironmentConfiguration) - { - configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_"); - } - - _config = configBuilder.Build(); - _builder.ConfigureHostConfiguration(config => { config.AddConfiguration(_config); @@ -172,51 +160,6 @@ private void ExecuteHostingStartups() } } - public IWebHost Build() - { - throw new NotSupportedException($"Building this implementation of {nameof(IWebHostBuilder)} is not supported."); - } - - public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) - { - _builder.ConfigureAppConfiguration((context, builder) => - { - var webhostBuilderContext = GetWebHostBuilderContext(context); - configureDelegate(webhostBuilderContext, builder); - }); - - return this; - } - - public IWebHostBuilder ConfigureServices(Action configureServices) - { - return ConfigureServices((context, services) => configureServices(services)); - } - - public IWebHostBuilder ConfigureServices(Action configureServices) - { - _builder.ConfigureServices((context, builder) => - { - var webhostBuilderContext = GetWebHostBuilderContext(context); - configureServices(webhostBuilderContext, builder); - }); - - return this; - } - - public IWebHostBuilder UseDefaultServiceProvider(Action configure) - { - _builder.UseServiceProviderFactory(context => - { - var webHostBuilderContext = GetWebHostBuilderContext(context); - var options = new ServiceProviderOptions(); - configure(webHostBuilderContext, options); - return new DefaultServiceProviderFactory(options); - }); - - return this; - } - public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType) { var startupAssemblyName = startupType.Assembly.GetName().Name; @@ -412,40 +355,6 @@ public IWebHostBuilder Configure(Action + { + config.AddConfiguration(_config); + }); + + _builder.ConfigureServices((context, services) => + { + var webhostContext = GetWebHostBuilderContext(context); + var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; + + // Add the IHostingEnvironment and IApplicationLifetime from Microsoft.AspNetCore.Hosting + services.AddSingleton(webhostContext.HostingEnvironment); +#pragma warning disable CS0618 // Type or member is obsolete + services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment); + services.AddSingleton(); +#pragma warning restore CS0618 // Type or member is obsolete + + services.Configure(options => + { + // Set the options + options.WebHostOptions = webHostOptions; + }); + + // REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up + // We need to flow this differently + services.TryAddSingleton(sp => new DiagnosticListener("Microsoft.AspNetCore")); + services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => new ActivitySource("Microsoft.AspNetCore")); + services.TryAddSingleton(DistributedContextPropagator.Current); + + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddSingleton(); + }); + } + + public IWebHostBuilder Configure(Action configure) + { + throw new NotSupportedException(); + } + + public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods)] Type startupType) + { + throw new NotSupportedException(); + } + + public IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(Func startupFactory) + { + throw new NotSupportedException(); + } + + public IWebHostBuilder Configure(Action configure) + { + var startupAssemblyName = configure.GetMethodInfo().DeclaringType!.Assembly.GetName().Name!; + + UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); + + _builder.ConfigureServices((context, services) => + { + services.Configure(options => + { + var webhostBuilderContext = GetWebHostBuilderContext(context); + options.ConfigureApplication = app => configure(webhostBuilderContext, app); + }); + }); + + return this; + } +} diff --git a/src/Hosting/Hosting/src/GenericHost/WebHostBuilderBase.cs b/src/Hosting/Hosting/src/GenericHost/WebHostBuilderBase.cs new file mode 100644 index 000000000000..c104ed40002a --- /dev/null +++ b/src/Hosting/Hosting/src/GenericHost/WebHostBuilderBase.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Hosting; + +internal abstract class WebHostBuilderBase : IWebHostBuilder, ISupportsUseDefaultServiceProvider +{ + private protected readonly IHostBuilder _builder; + private protected readonly IConfiguration _config; + + public WebHostBuilderBase(IHostBuilder builder, WebHostBuilderOptions options) + { + _builder = builder; + var configBuilder = new ConfigurationBuilder() + .AddInMemoryCollection(); + + if (!options.SuppressEnvironmentConfiguration) + { + configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_"); + } + + _config = configBuilder.Build(); + } + + public IWebHost Build() + { + throw new NotSupportedException($"Building this implementation of {nameof(IWebHostBuilder)} is not supported."); + } + + public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _builder.ConfigureAppConfiguration((context, builder) => + { + var webhostBuilderContext = GetWebHostBuilderContext(context); + configureDelegate(webhostBuilderContext, builder); + }); + + return this; + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + return ConfigureServices((context, services) => configureServices(services)); + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + _builder.ConfigureServices((context, builder) => + { + var webhostBuilderContext = GetWebHostBuilderContext(context); + configureServices(webhostBuilderContext, builder); + }); + + return this; + } + + public IWebHostBuilder UseDefaultServiceProvider(Action configure) + { + _builder.UseServiceProviderFactory(context => + { + var webHostBuilderContext = GetWebHostBuilderContext(context); + var options = new ServiceProviderOptions(); + configure(webHostBuilderContext, options); + return new DefaultServiceProviderFactory(options); + }); + + return this; + } + + protected WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context) + { + if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal)) + { + // Use _config as a fallback for WebHostOptions in case the chained source was removed from the hosting IConfigurationBuilder. + var options = new WebHostOptions(context.Configuration, fallbackConfiguration: _config, environment: context.HostingEnvironment); + var webHostBuilderContext = new WebHostBuilderContext + { + Configuration = context.Configuration, + HostingEnvironment = new HostingEnvironment(), + }; + webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options, baseEnvironment: context.HostingEnvironment); + context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext; + context.Properties[typeof(WebHostOptions)] = options; + return webHostBuilderContext; + } + + // Refresh config, it's periodically updated/replaced + var webHostContext = (WebHostBuilderContext)contextVal; + webHostContext.Configuration = context.Configuration; + return webHostContext; + } + + public string? GetSetting(string key) + { + return _config[key]; + } + + public IWebHostBuilder UseSetting(string key, string? value) + { + _config[key] = value; + return this; + } +} diff --git a/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs b/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs index 7bd6f86a49af..ea37f73aa036 100644 --- a/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs +++ b/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs @@ -33,6 +33,35 @@ public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, ActionThe delegate that configures the . /// The . public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action configure, Action configureWebHostBuilder) + { + return ConfigureWebHost( + builder, + static (hostBuilder, options) => new GenericWebHostBuilder(hostBuilder, options), + configure, + configureWebHostBuilder); + } + + /// + /// Adds and configures an ASP.NET Core web application with minimal dependencies. + /// + /// The to add the to. + /// The delegate that configures the . + /// The delegate that configures the . + /// The . + public static IHostBuilder ConfigureSlimWebHost(this IHostBuilder builder, Action configure, Action configureWebHostBuilder) + { + return ConfigureWebHost( + builder, + static (hostBuilder, options) => new SlimWebHostBuilder(hostBuilder, options), + configure, + configureWebHostBuilder); + } + + private static IHostBuilder ConfigureWebHost( + this IHostBuilder builder, + Func createWebHostBuilder, + Action configure, + Action configureWebHostBuilder) { ArgumentNullException.ThrowIfNull(configure); ArgumentNullException.ThrowIfNull(configureWebHostBuilder); @@ -45,7 +74,7 @@ public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action services.AddHostedService()); return builder; diff --git a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..de9a58f9e603 100644 --- a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt +++ b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions.ConfigureSlimWebHost(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action! configure, System.Action! configureWebHostBuilder) -> Microsoft.Extensions.Hosting.IHostBuilder! diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs index 582154c971a8..2d01289cdf19 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs @@ -1,7 +1,6 @@ // 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.Text.Json.Serialization; using Microsoft.AspNetCore.Http; @@ -13,8 +12,6 @@ namespace Microsoft.AspNetCore.Mvc; [JsonConverter(typeof(ProblemDetailsJsonConverter))] public class ProblemDetails { - private readonly IDictionary _extensions = new Dictionary(StringComparer.Ordinal); - /// /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when /// dereferenced, it provide human-readable documentation for the problem type @@ -62,10 +59,5 @@ public class ProblemDetails /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. /// [JsonExtensionData] - public IDictionary Extensions - { - [RequiresUnreferencedCode("JSON serialization and deserialization of ProblemDetails.Extensions might require types that cannot be statically analyzed.")] - [RequiresDynamicCode("JSON serialization and deserialization of ProblemDetails.Extensions might require types that cannot be statically analyzed.")] - get => _extensions; - } + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); } diff --git a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs index 81e4abee0635..666303b71df9 100644 --- a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; @@ -92,6 +93,39 @@ public void Read_UsingJsonSerializerWorks() }); } + [Fact] + public void Read_WithUnknownTypeHandling_Works() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5"; + var title = "Not found"; + var status = 404; + var detail = "Product not found"; + var instance = "http://example.com/products/14"; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; + var serializerOptions = new JsonSerializerOptions(JsonSerializerOptions) { UnknownTypeHandling = System.Text.Json.Serialization.JsonUnknownTypeHandling.JsonNode }; + + // Act + var problemDetails = JsonSerializer.Deserialize(json, serializerOptions); + + // Assert + Assert.NotNull(problemDetails); + Assert.Equal(type, problemDetails!.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Equal(instance, problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.IsAssignableFrom(kvp.Value!); + Assert.Equal(traceId, kvp.Value?.ToString()); + }); + } + [Fact] public void Read_WithSomeMissingValues_Works() { @@ -178,4 +212,36 @@ public void Write_WithSomeMissingContent_Works() var actual = Encoding.UTF8.GetString(stream.ToArray()); Assert.Equal(expected, actual); } + + [Fact] + public void Write_WithNullExtensionValue_Works() + { + // Arrange + var value = new ProblemDetails + { + Title = "Not found", + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.5", + Status = 404, + Detail = "Product not found", + Instance = "http://example.com/products/14", + Extensions = + { + { "traceId", null }, + { "some-data", new[] { "value1", "value2" } } + } + }; + var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status},\"detail\":\"{value.Detail}\",\"instance\":\"{JsonEncodedText.Encode(value.Instance)}\",\"traceId\":null,\"some-data\":[\"value1\",\"value2\"]}}"; + var converter = new ProblemDetailsJsonConverter(); + var stream = new MemoryStream(); + + // Act + using (var writer = new Utf8JsonWriter(stream)) + { + converter.Write(writer, value, JsonSerializerOptions); + } + + // Assert + var actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, actual); + } } diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 2b3c14d62cdc..529e7e6f4b4d 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -20,7 +20,6 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator "MapPut", "MapDelete", "MapPatch", - "Map", }; public void Initialize(IncrementalGeneratorInitializationContext context) @@ -46,48 +45,54 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .WithTrackingName("EndpointModel"); var thunks = endpoints.Select((endpoint, _) => $$""" - [{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( - (del, builder) => +[{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( + (methodInfo, options) => { - builder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + if (options == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, - (del, builder) => + (del, options, inferredMetadataResult) => { var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; EndpointFilterDelegate? filteredInvocation = null; - if (builder.FilterFactories.Count > 0) + if (options.EndpointBuilder.FilterFactories.Count > 0) { filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { if (ic.HttpContext.Response.StatusCode == 400) { - return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + return ValueTask.FromResult(Results.Empty); } {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} }, - builder, + options.EndpointBuilder, handler.Method); } {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} - return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); }), """); var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" -{{RequestDelegateGeneratorSources.GeneratedCodeAttribute}} -internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( - this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, - [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, - {{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) - { - return GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); - } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::{{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {{StaticRouteHandlerModelEmitter.EmitVerb(endpoint)}}, filePath, lineNumber); + } """); var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 800e4c21e761..6cb436e4d9e9 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -15,26 +15,9 @@ internal static class RequestDelegateGeneratorSources // //------------------------------------------------------------------------------ #nullable enable -using global::System; -using global::System.Collections; -using global::System.Collections.Generic; -using global::System.Diagnostics; -using global::System.Linq; -using global::System.Reflection; -using global::System.Threading.Tasks; -using global::System.IO; -using global::Microsoft.AspNetCore.Routing; -using global::Microsoft.AspNetCore.Routing.Patterns; -using global::Microsoft.AspNetCore.Builder; -using global::Microsoft.AspNetCore.Http; -using global::Microsoft.Extensions.DependencyInjection; -using global::Microsoft.Extensions.FileProviders; -using global::Microsoft.Extensions.Primitives; -using MetadataPopulator = System.Action; -using RequestDelegateFactoryFunc = System.Func; """; - public static string GeneratedCodeAttribute => $@"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]"; + public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]"; public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$""" {{SourceHeader}} @@ -53,360 +36,140 @@ public SourceKey(string path, int line) Line = line; } } -} - -{{GeneratedCodeAttribute}} -// This class needs to be internal so that the compiled application -// has access to the strongly-typed endpoint definitions that are -// generated by the compiler so that they will be favored by -// overload resolution and opt the runtime in to the code generated -// implementation produced here. -internal static class GenerateRouteBuilderEndpoints -{ - private static readonly string[] GetVerb = new[] { HttpMethods.Get }; - private static readonly string[] PostVerb = new[] { HttpMethods.Post }; - private static readonly string[] PutVerb = new[] { HttpMethods.Put }; - private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; - private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; - {{endpoints}} +{{GetEndpoints(endpoints)}} } -{{GeneratedCodeAttribute}} -file static class GeneratedRouteBuilderExtensionsCore +namespace Microsoft.AspNetCore.Http.Generated { - internal static class GenericThunks - { - public static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{genericThunks}} - }; - } - - internal static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{thunks}} - }; - - internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, - string pattern, - System.Delegate handler, - IEnumerable httpMethods, - string filePath, - int lineNumber) - { - var (populate, factory) = GenericThunks.map[(filePath, lineNumber)]; - return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); - } - - internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, - string pattern, - System.Delegate handler, - IEnumerable httpMethods, - string filePath, - int lineNumber) - { - var (populate, factory) = map[(filePath, lineNumber)]; - return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); - } - - internal static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataSource(IEndpointRouteBuilder endpoints) - { - SourceGeneratedRouteEndpointDataSource? routeEndpointDataSource = null; - foreach (var dataSource in endpoints.DataSources) - { - if (dataSource is SourceGeneratedRouteEndpointDataSource foundDataSource) + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { +{{GetGenericThunks(genericThunks)}} +{{GetThunks(thunks)}} + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) { - routeEndpointDataSource = foundDataSource; - break; + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); } + return filteredInvocation; } - if (routeEndpointDataSource is null) - { - routeEndpointDataSource = new SourceGeneratedRouteEndpointDataSource(endpoints.ServiceProvider); - endpoints.DataSources.Add(routeEndpointDataSource); - } - return routeEndpointDataSource; - } - internal static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, global::System.Reflection.MethodInfo mi) - { - var routeHandlerFilters = builder.FilterFactories; - var context0 = new EndpointFilterFactoryContext - { - MethodInfo = mi, - ApplicationServices = builder.ApplicationServices, - }; - var initialFilteredInvocation = filteredInvocation; - for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider { - var filterFactory = routeHandlerFilters[i]; - filteredInvocation = filterFactory(context0, filteredInvocation); + T.PopulateMetadata(method, builder); } - return filteredInvocation; - } - - internal static void PopulateMetadata(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - internal static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - - internal static Task ExecuteObjectResult(object? obj, HttpContext httpContext) - { - if (obj is IResult r) - { - return r.ExecuteAsync(httpContext); - } - else if (obj is string s) - { - return httpContext.Response.WriteAsync(s); - } - else - { - return httpContext.Response.WriteAsJsonAsync(obj); - } - } -} - -{{GeneratedCodeAttribute}} -file class SourceGeneratedRouteEndpointDataSource : EndpointDataSource -{ - private readonly List _routeEntries = new(); - private readonly IServiceProvider _applicationServices; - - public SourceGeneratedRouteEndpointDataSource(IServiceProvider applicationServices) - { - _applicationServices = applicationServices; - } - - public RouteHandlerBuilder AddRouteHandler( - RoutePattern pattern, - Delegate routeHandler, - IEnumerable httpMethods, - bool isFallback, - MetadataPopulator metadataPopulator, - RequestDelegateFactoryFunc requestDelegateFactoryFunc) - { - var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); - var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); - var routeAttributes = RouteAttributes.RouteHandler; - - if (isFallback) + private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider { - routeAttributes |= RouteAttributes.Fallback; + T.PopulateMetadata(parameter, builder); } - _routeEntries.Add(new() - { - RoutePattern = pattern, - RouteHandler = routeHandler, - HttpMethods = httpMethods, - RouteAttributes = routeAttributes, - Conventions = conventions, - FinallyConventions = finallyConventions, - RequestDelegateFactory = requestDelegateFactoryFunc, - MetadataPopulator = metadataPopulator, - }); - return new RouteHandlerBuilder(new[] { new ConventionBuilder(conventions, finallyConventions) }); - } - public override IReadOnlyList Endpoints - { - get + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { - var endpoints = new RouteEndpoint[_routeEntries.Count]; - for (int i = 0; i < _routeEntries.Count; i++) + if (obj is IResult r) { - endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i]).Build(); + return r.ExecuteAsync(httpContext); } - return endpoints; - } - } - - public override IReadOnlyList GetGroupedEndpoints(RouteGroupContext context) - { - var endpoints = new RouteEndpoint[_routeEntries.Count]; - for (int i = 0; i < _routeEntries.Count; i++) - { - endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions, context.FinallyConventions).Build(); - } - return endpoints; - } - - public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; - - private RouteEndpointBuilder CreateRouteEndpointBuilder( - RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList>? groupConventions = null, IReadOnlyList>? groupFinallyConventions = null) - { - var pattern = RoutePatternFactory.Combine(groupPrefix, entry.RoutePattern); - var handler = entry.RouteHandler; - var isRouteHandler = (entry.RouteAttributes & RouteAttributes.RouteHandler) == RouteAttributes.RouteHandler; - var isFallback = (entry.RouteAttributes & RouteAttributes.Fallback) == RouteAttributes.Fallback; - var order = isFallback ? int.MaxValue : 0; - var displayName = pattern.RawText ?? pattern.ToString(); - if (entry.HttpMethods is not null) - { - // Prepends the HTTP method to the DisplayName produced with pattern + method name - displayName = $"HTTP: {string.Join("", "", entry.HttpMethods)} {displayName}"; - } - if (isFallback) - { - displayName = $"Fallback {displayName}"; - } - // If we're not a route handler, we started with a fully realized (although unfiltered) RequestDelegate, so we can just redirect to that - // while running any conventions. We'll put the original back if it remains unfiltered right before building the endpoint. - RequestDelegate? factoryCreatedRequestDelegate = null; - // Let existing conventions capture and call into builder.RequestDelegate as long as they do so after it has been created. - RequestDelegate redirectRequestDelegate = context => - { - if (factoryCreatedRequestDelegate is null) + else if (obj is string s) { - throw new InvalidOperationException("Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild"); + return httpContext.Response.WriteAsync(s); } - return factoryCreatedRequestDelegate(context); - }; - // Add MethodInfo and HttpMethodMetadata (if any) as first metadata items as they are intrinsic to the route much like - // the pattern or default display name. This gives visibility to conventions like WithOpenApi() to intrinsic route details - // (namely the MethodInfo) even when applied early as group conventions. - RouteEndpointBuilder builder = new(redirectRequestDelegate, pattern, order) - { - DisplayName = displayName, - ApplicationServices = _applicationServices, - }; - if (isRouteHandler) - { - builder.Metadata.Add(handler.Method); - } - if (entry.HttpMethods is not null) - { - builder.Metadata.Add(new HttpMethodMetadata(entry.HttpMethods)); - } - // Apply group conventions before entry-specific conventions added to the RouteHandlerBuilder. - if (groupConventions is not null) - { - foreach (var groupConvention in groupConventions) + else { - groupConvention(builder); + return httpContext.Response.WriteAsJsonAsync(obj); } } - // Any metadata inferred directly inferred by RDF or indirectly inferred via IEndpoint(Parameter)MetadataProviders are - // considered less specific than method-level attributes and conventions but more specific than group conventions - // so inferred metadata gets added in between these. If group conventions need to override inferred metadata, - // they can do so via IEndpointConventionBuilder.Finally like the do to override any other entry-specific metadata. - if (isRouteHandler) - { - entry.MetadataPopulator(entry.RouteHandler, builder); - } - // Add delegate attributes as metadata before entry-specific conventions but after group conventions. - var attributes = handler.Method.GetCustomAttributes(); - if (attributes is not null) - { - foreach (var attribute in attributes) + } +} +"""; + private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$""" + private static class GenericThunks { - builder.Metadata.Add(attribute); - } - } - entry.Conventions.IsReadOnly = true; - foreach (var entrySpecificConvention in entry.Conventions) - { - entrySpecificConvention(builder); - } - // If no convention has modified builder.RequestDelegate, we can use the RequestDelegate returned by the RequestDelegateFactory directly. - var conventionOverriddenRequestDelegate = ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate) ? null : builder.RequestDelegate; - if (isRouteHandler || builder.FilterFactories.Count > 0) - { - factoryCreatedRequestDelegate = entry.RequestDelegateFactory(entry.RouteHandler, builder); + public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{genericThunks}} + }; } - Debug.Assert(factoryCreatedRequestDelegate is not null); - // Use the overridden RequestDelegate if it exists. If the overridden RequestDelegate is merely wrapping the final RequestDelegate, - // it will still work because of the redirectRequestDelegate. - builder.RequestDelegate = conventionOverriddenRequestDelegate ?? factoryCreatedRequestDelegate; - entry.FinallyConventions.IsReadOnly = true; - foreach (var entryFinallyConvention in entry.FinallyConventions) + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) { - entryFinallyConvention(builder); + var (populateMetadata, createRequestDelegate) = GenericThunks.map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); } - if (groupFinallyConventions is not null) - { - // Group conventions are ordered by the RouteGroupBuilder before - // being provided here. - foreach (var groupFinallyConvention in groupFinallyConventions) - { - groupFinallyConvention(builder); - } - } - return builder; - } - - private readonly struct RouteEntry - { - public MetadataPopulator MetadataPopulator { get; init; } - public RequestDelegateFactoryFunc RequestDelegateFactory { get; init; } - public RoutePattern RoutePattern { get; init; } - public Delegate RouteHandler { get; init; } - public IEnumerable HttpMethods { get; init; } - public RouteAttributes RouteAttributes { get; init; } - public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; } - public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions { get; init; } - } +""" : string.Empty; - [Flags] - private enum RouteAttributes - { - // The endpoint was defined by a RequestDelegate, RequestDelegateFactory.Create() should be skipped unless there are endpoint filters. - None = 0, - // This was added as Delegate route handler, so RequestDelegateFactory.Create() should always be called. - RouteHandler = 1, - // This was added by MapFallback. - Fallback = 2, - } -} - -// This file class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. -{{GeneratedCodeAttribute}} -file class ThrowOnAddAfterEndpointBuiltConventionCollection : List>, ICollection> -{ - // We throw if someone tries to add conventions to the RouteEntry after endpoints have already been resolved meaning the conventions - // will not be observed given RouteEndpointDataSource is not meant to be dynamic and uses NullChangeToken.Singleton. - public bool IsReadOnly { get; set; } - void ICollection>.Add(Action convention) - { - if (IsReadOnly) + private static string GetThunks(string thunks) => thunks != string.Empty ? $$""" + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - throw new InvalidOperationException("Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild"); - } - Add(convention); - } -} + {{thunks}} + }; -{{GeneratedCodeAttribute}} -file class ConventionBuilder : IEndpointConventionBuilder -{ - private readonly ICollection> _conventions; - private readonly ICollection> _finallyConventions; - public ConventionBuilder(ICollection> conventions, ICollection> finallyConventions) - { - _conventions = conventions; - _finallyConventions = finallyConventions; - } - /// - /// Adds the specified convention to the builder. Conventions are used to customize instances. - /// - /// The convention to add to the builder. - public void Add(Action convention) - { - _conventions.Add(convention); - } - public void Finally(Action finalConvention) + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } +""" : string.Empty; + + private static string GetEndpoints(string endpoints) => endpoints != string.Empty ? $$""" + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + {{GeneratedCodeAttribute}} + internal static class GenerateRouteBuilderEndpoints { - _finallyConventions.Add(finalConvention); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + +{{endpoints}} } -} -"""; +""" : string.Empty; } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 2e1e9249e635..996cec75e731 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.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; + namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelEmitter @@ -28,6 +30,19 @@ public static string EmitSourceKey(Endpoint endpoint) return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; } + public static string EmitVerb(Endpoint endpoint) + { + return endpoint.HttpMethod switch + { + "MapGet" => "GetVerb", + "MapPut" => "PutVerb", + "MapPost" => "PostVerb", + "MapDelete" => "DeleteVerb", + "MapPatch" => "PatchVerb", + _ => throw new ArgumentException($"Received unexpected HTTP method: {endpoint.HttpMethod}") + }; + } + /* * TODO: Emit invocation to the request handler. The structure * involved here consists of a call to bind parameters, check @@ -37,7 +52,7 @@ public static string EmitSourceKey(Endpoint endpoint) public static string EmitRequestHandler() { return """ -System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) +Task RequestHandler(HttpContext httpContext) { var result = handler(); return httpContext.Response.WriteAsync(result); @@ -55,7 +70,7 @@ System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext public static string EmitFilteredRequestHandler() { return """ -async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) +async Task RequestHandlerFiltered(HttpContext httpContext) { var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); @@ -81,6 +96,6 @@ async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Ht */ public static string EmitFilteredInvocation() { - return "return System.Threading.Tasks.ValueTask.FromResult(handler());"; + return "return ValueTask.FromResult(handler());"; } } diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index 09cc97339d0b..437283c330dd 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -32,13 +32,14 @@ public static class HttpRequestJsonExtensions /// The request to read from. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static ValueTask ReadFromJsonAsync( this HttpRequest request, CancellationToken cancellationToken = default) { - return request.ReadFromJsonAsync(options: null, cancellationToken); + ArgumentNullException.ThrowIfNull(request); + + var options = ResolveSerializerOptions(request.HttpContext); + return request.ReadFromJsonAsync(jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), cancellationToken); } /// @@ -166,14 +167,15 @@ public static class HttpRequestJsonExtensions /// The type of object to read. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static ValueTask ReadFromJsonAsync( this HttpRequest request, Type type, CancellationToken cancellationToken = default) { - return request.ReadFromJsonAsync(type, options: null, cancellationToken); + ArgumentNullException.ThrowIfNull(request); + + var options = ResolveSerializerOptions(request.HttpContext); + return request.ReadFromJsonAsync(jsonTypeInfo: options.GetTypeInfo(type), cancellationToken); } /// diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 9d771adc2fb2..d2ec3b1e516e 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -30,14 +30,15 @@ public static partial class HttpResponseJsonExtensions /// The value to write as JSON. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static Task WriteAsJsonAsync( this HttpResponse response, TValue value, CancellationToken cancellationToken = default) { - return response.WriteAsJsonAsync(value, options: null, contentType: null, cancellationToken); + ArgumentNullException.ThrowIfNull(response); + + var options = ResolveSerializerOptions(response.HttpContext); + return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo)options.GetTypeInfo(typeof(TValue)), contentType: null, cancellationToken); } /// @@ -203,15 +204,16 @@ private static async Task WriteAsJsonAsyncSlow( /// The type of object to write. /// A used to cancel the operation. /// The task object representing the asynchronous operation. - [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] - [RequiresDynamicCode(RequiresDynamicCodeMessage)] public static Task WriteAsJsonAsync( this HttpResponse response, object? value, Type type, CancellationToken cancellationToken = default) { - return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken); + ArgumentNullException.ThrowIfNull(response); + + var options = ResolveSerializerOptions(response.HttpContext); + return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetTypeInfo(type), contentType: null, cancellationToken); } /// diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index a47c77af8f96..5ca45dc9faff 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -4,6 +4,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Internal; #nullable enable @@ -26,7 +27,7 @@ public class JsonOptions // The JsonSerializerOptions.GetTypeInfo method is called directly and needs a defined resolver // setting the default resolver (reflection-based) but the user can overwrite it directly or calling // .AddContext() - TypeInfoResolver = CreateDefaultTypeResolver() + TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver() }; // Use a copy so the defaults are not modified. diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 65576652a378..9ac6d360d2b7 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -12,17 +12,18 @@ - - - - + + + + - + - - + + + @@ -36,4 +37,5 @@ + diff --git a/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs b/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs index b247e359e483..287b7d93d954 100644 --- a/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs +++ b/src/Http/Http.Extensions/src/ProblemDetailsJsonContext.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Http; [JsonSerializable(typeof(ProblemDetails))] [JsonSerializable(typeof(HttpValidationProblemDetails))] +// ExtensionData +[JsonSerializable(typeof(IDictionary))] // Additional values are specified on JsonSerializerContext to support some values for extensions. // For example, the DeveloperExceptionMiddleware serializes its complex type to JsonElement, which problem details then needs to serialize. [JsonSerializable(typeof(JsonElement))] diff --git a/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml b/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..92c77d69a9e6 --- /dev/null +++ b/src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index b697d5c7c801..409daf83f94a 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -269,7 +269,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat Handler = handler, ServiceProvider = serviceProvider, ServiceProviderIsService = serviceProvider.GetService(), - RouteParameters = options?.RouteParameterNames?.ToList(), + RouteParameters = options?.RouteParameterNames, ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, EndpointBuilder = endpointBuilder, diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs index 4067434bef1e..04c5d526f635 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs @@ -14,7 +14,7 @@ internal sealed class RequestDelegateFactoryContext // Options public required IServiceProvider ServiceProvider { get; init; } public required IServiceProviderIsService? ServiceProviderIsService { get; init; } - public required List? RouteParameters { get; init; } + public required IEnumerable? RouteParameters { get; init; } public required bool ThrowOnBadRequest { get; init; } public required bool DisableInferredFromBody { get; init; } public required EndpointBuilder EndpointBuilder { get; init; } diff --git a/src/Http/Http.Extensions/test/JsonOptionsTests.cs b/src/Http/Http.Extensions/test/JsonOptionsTests.cs new file mode 100644 index 000000000000..2bae1a700e66 --- /dev/null +++ b/src/Http/Http.Extensions/test/JsonOptionsTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Http.Extensions; + +public class JsonOptionsTests +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_SetsTypeInfoResolverNull_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = JsonOptions.DefaultSerializerOptions; + + // Assert + Assert.Null(options.TypeInfoResolver); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_SetsTypeInfoResolverToDefault_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = JsonOptions.DefaultSerializerOptions; + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + }, options); + } +} diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 7d43a3aa61bb..95942fd9ed66 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -24,4 +24,10 @@ + + + + PreserveNewest + + diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt new file mode 100644 index 000000000000..8309dc890cee --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -0,0 +1,183 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace Microsoft.AspNetCore.Builder +{ + %GENERATEDCODEATTRIBUTE% + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } + + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + %GENERATEDCODEATTRIBUTE% + internal static class GenerateRouteBuilderEndpoints + { + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + } + + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { + + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + [(@"TestMapActions.cs", 15)] = ( + (methodInfo, options) => + { + if (options == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var result = handler(); + return httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + + }; + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider + { + T.PopulateMetadata(method, builder); + } + + private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider + { + T.PopulateMetadata(parameter, builder); + } + + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return httpContext.Response.WriteAsJsonAsync(obj); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 80ea89c6b40f..87fe135e619b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Text; using Microsoft.AspNetCore.Builder; @@ -36,7 +37,7 @@ internal static (ImmutableArray, Compilation) RunGenerator(s driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var _); var diagnostics = updatedCompilation.GetDiagnostics(); - Assert.Empty(diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + Assert.Empty(diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning)); var runResult = driver.GetRunResult(); return (runResult.Results, updatedCompilation); @@ -172,6 +173,48 @@ public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder return compilation; } + internal async Task VerifyAgainstBaselineUsingFile(Compilation compilation, [CallerMemberName] string callerName = "") + { + var baselineFilePath = Path.Combine("RequestDelegateGenerator", "Baselines", $"{callerName}.generated.txt"); + var generatedCode = compilation.SyntaxTrees.Last(); + var baseline = await File.ReadAllTextAsync(baselineFilePath); + var expectedLines = baseline + .TrimEnd() // Trim newlines added by autoformat + .Replace("%GENERATEDCODEATTRIBUTE%", RequestDelegateGeneratorSources.GeneratedCodeAttribute) + .Split(Environment.NewLine); + + Assert.True(CompareLines(expectedLines, generatedCode.GetText(), out var errorMessage), errorMessage); + } + + private bool CompareLines(string[] expectedLines, SourceText sourceText, out string message) + { + if (expectedLines.Length != sourceText.Lines.Count) + { + message = $"Line numbers do not match. Expected: {expectedLines.Length} lines, but generated {sourceText.Lines.Count}"; + return false; + } + var index = 0; + foreach (var textLine in sourceText.Lines) + { + var expectedLine = expectedLines[index].Trim().ReplaceLineEndings(); + var actualLine = textLine.ToString().Trim().ReplaceLineEndings(); + if (!expectedLine.Equals(actualLine, StringComparison.Ordinal)) + { + message = $""" +Line {textLine.LineNumber} does not match. +Expected Line: +{expectedLine} +Actual Line: +{textLine} +"""; + return false; + } + index++; + } + message = string.Empty; + return true; + } + private sealed class AppLocalResolver : ICompilationAssemblyResolver { public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 606624f589db..18db6f48d6bf 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -39,6 +39,42 @@ public async Task MapAction_NoParam_StringReturn(string source, string expectedB Assert.Equal(expectedBody, body); } + [Fact] + public async Task MapGet_NoParam_StringReturn_WithFilter() + { + var source = """ +app.MapGet("/hello", () => "Hello world!") + .AddEndpointFilter(async (context, next) => { + var result = await next(context); + return $"Filtered: {result}"; + }); +"""; + var expectedBody = "Filtered: Hello world!"; + var (results, compilation) = RunGenerator(source); + + await VerifyAgainstBaselineUsingFile(compilation); + + var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/hello", endpointModel.Route.RoutePattern); + + var httpContext = new DefaultHttpContext(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + [Theory] [InlineData("""app.MapGet("/hello", () => 2);""")] [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 648356d1a304..a05c8006992a 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -439,7 +439,7 @@ private static RouteHandlerBuilder Map( .AddRouteHandler(pattern, handler, httpMethods, isFallback, RequestDelegateFactory.InferMetadata, RequestDelegateFactory.Create); } - private static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints) + internal static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints) { RouteEndpointDataSource? routeEndpointDataSource = null; diff --git a/src/Http/Routing/src/Builder/RouteHandlerServices.cs b/src/Http/Routing/src/Builder/RouteHandlerServices.cs new file mode 100644 index 000000000000..e3b700a70a6b --- /dev/null +++ b/src/Http/Routing/src/Builder/RouteHandlerServices.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides methods used for invoking the route endpoint +/// infrastructure with custom funcs for populating metadata +/// and creating request delegates. Intended to be consumed from +/// the RequestDeleatgeGenerator only. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class RouteHandlerServices +{ + /// + /// Registers an endpoint with custom functions for constructing + /// a request delegate for its handler and populating metadata for + /// the endpoint. Intended for consumption in the RequestDelegateGenerator. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// The set of supported HTTP methods. May not be null. + /// A delegate for populating endpoint metadata. + /// A delegate for constructing a RequestDelegate. + /// + public static RouteHandlerBuilder Map( + IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler, + IEnumerable httpMethods, + Func populateMetadata, + Func createRequestDelegate) + { + ArgumentNullException.ThrowIfNull(endpoints); + ArgumentNullException.ThrowIfNull(pattern); + ArgumentNullException.ThrowIfNull(handler); + ArgumentNullException.ThrowIfNull(populateMetadata); + ArgumentNullException.ThrowIfNull(createRequestDelegate); + + return endpoints + .GetOrAddRouteEndpointDataSource() + .AddRouteHandler(RoutePatternFactory.Parse(pattern), + handler, + httpMethods, + isFallback: false, + populateMetadata, + createRequestDelegate); + } +} diff --git a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs index 465e0a9f9c5d..50963a0df25f 100644 --- a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs @@ -1,6 +1,9 @@ // 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.Text.RegularExpressions; + namespace Microsoft.AspNetCore.Routing.Constraints; /// @@ -12,7 +15,7 @@ public class RegexInlineRouteConstraint : RegexRouteConstraint /// Initializes a new instance of the class. /// /// The regular expression pattern to match. - public RegexInlineRouteConstraint(string regexPattern) + public RegexInlineRouteConstraint([StringSyntax(StringSyntaxAttribute.Regex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)] string regexPattern) : base(regexPattern) { } diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index 8ec928d6ae83..af4f42989d2f 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -154,25 +154,27 @@ private static EdgeKey CreateEdgeKey(string host) return EdgeKey.WildcardEdgeKey; } - var hostParts = host.Split(':'); - if (hostParts.Length == 1) + Span hostParts = stackalloc Range[3]; + var hostSpan = host.AsSpan(); + var length = hostSpan.Split(hostParts, ':'); + if (length == 1) { - if (!string.IsNullOrEmpty(hostParts[0])) + if (!hostSpan[hostParts[0]].IsEmpty) { - return new EdgeKey(hostParts[0], null); + return new EdgeKey(hostSpan[hostParts[0]].ToString(), null); } } - if (hostParts.Length == 2) + if (length == 2) { - if (!string.IsNullOrEmpty(hostParts[0])) + if (!hostSpan[hostParts[0]].IsEmpty) { - if (int.TryParse(hostParts[1], out var port)) + if (int.TryParse(hostSpan[hostParts[1]], out var port)) { - return new EdgeKey(hostParts[0], port); + return new EdgeKey(hostSpan[hostParts[0]].ToString(), port); } - else if (string.Equals(hostParts[1], WildcardHost, StringComparison.Ordinal)) + else if (hostSpan[hostParts[1]].Equals(WildcardHost, StringComparison.Ordinal)) { - return new EdgeKey(hostParts[0], null); + return new EdgeKey(hostSpan[hostParts[0]].ToString(), null); } } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..a552550b5e1c 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Routing.RouteHandlerServices +static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable! httpMethods, System.Func! populateMetadata, System.Func! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! diff --git a/src/Http/Routing/src/RouteEndpointDataSource.cs b/src/Http/Routing/src/RouteEndpointDataSource.cs index 96fbdf6c6cb1..4fc6cac61fe7 100644 --- a/src/Http/Routing/src/RouteEndpointDataSource.cs +++ b/src/Http/Routing/src/RouteEndpointDataSource.cs @@ -262,20 +262,22 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( private RequestDelegateFactoryOptions CreateRdfOptions(RouteEntry entry, RoutePattern pattern, RouteEndpointBuilder builder) { - var routeParamNames = new List(pattern.Parameters.Count); - foreach (var parameter in pattern.Parameters) - { - routeParamNames.Add(parameter.Name); - } - return new() { ServiceProvider = _applicationServices, - RouteParameterNames = routeParamNames, + RouteParameterNames = ProduceRouteParamNames(), ThrowOnBadRequest = _throwOnBadRequest, DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods), EndpointBuilder = builder, }; + + IEnumerable ProduceRouteParamNames() + { + foreach (var routePatternPart in pattern.Parameters) + { + yield return routePatternPart.Name; + } + } } private static bool ShouldDisableInferredBodyParameters(IEnumerable? httpMethods) diff --git a/src/Identity/Extensions.Stores/src/UserStoreBase.cs b/src/Identity/Extensions.Stores/src/UserStoreBase.cs index 0a3a7103c9c4..3f32e4da2408 100644 --- a/src/Identity/Extensions.Stores/src/UserStoreBase.cs +++ b/src/Identity/Extensions.Stores/src/UserStoreBase.cs @@ -920,7 +920,20 @@ public virtual async Task CountCodesAsync(TUser user, CancellationToken can var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken).ConfigureAwait(false) ?? ""; if (mergedCodes.Length > 0) { - return mergedCodes.Split(';').Length; + // non-allocating version of mergedCodes.Split(';').Length + var count = 1; + var index = 0; + while (index < mergedCodes.Length) + { + var semiColonIndex = mergedCodes.IndexOf(';', index); + if (semiColonIndex < 0) + { + break; + } + count++; + index = semiColonIndex + 1; + } + return count; } return 0; } diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs index 91e74ef529a6..bb34c6d64597 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs @@ -202,8 +202,6 @@ await _problemDetailsService.WriteAsync(new() } } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Values set on ProblemDetails.Extensions are supported by the default writer.")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Values set on ProblemDetails.Extensions are supported by the default writer.")] private ProblemDetails CreateProblemDetails(ErrorContext errorContext, HttpContext httpContext) { var problemDetails = new ProblemDetails diff --git a/src/Middleware/Localization/src/CookieRequestCultureProvider.cs b/src/Middleware/Localization/src/CookieRequestCultureProvider.cs index d42efdba75e5..9467de05376b 100644 --- a/src/Middleware/Localization/src/CookieRequestCultureProvider.cs +++ b/src/Middleware/Localization/src/CookieRequestCultureProvider.cs @@ -69,15 +69,15 @@ public static string MakeCookieValue(RequestCulture requestCulture) return null; } - var parts = value.Split(_cookieSeparator, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length != 2) + Span parts = stackalloc Range[3]; + var valueSpan = value.AsSpan(); + if (valueSpan.Split(parts, _cookieSeparator, StringSplitOptions.RemoveEmptyEntries) != 2) { return null; } - var potentialCultureName = parts[0]; - var potentialUICultureName = parts[1]; + var potentialCultureName = valueSpan[parts[0]]; + var potentialUICultureName = valueSpan[parts[1]]; if (!potentialCultureName.StartsWith(_culturePrefix, StringComparison.Ordinal) || ! potentialUICultureName.StartsWith(_uiCulturePrefix, StringComparison.Ordinal)) @@ -85,26 +85,26 @@ public static string MakeCookieValue(RequestCulture requestCulture) return null; } - var cultureName = potentialCultureName.Substring(_culturePrefix.Length); - var uiCultureName = potentialUICultureName.Substring(_uiCulturePrefix.Length); + var cultureName = potentialCultureName.Slice(_culturePrefix.Length); + var uiCultureName = potentialUICultureName.Slice(_uiCulturePrefix.Length); - if (cultureName == null && uiCultureName == null) + if (cultureName.IsEmpty && uiCultureName.IsEmpty) { // No values specified for either so no match return null; } - if (cultureName != null && uiCultureName == null) + if (!cultureName.IsEmpty && uiCultureName.IsEmpty) { // Value for culture but not for UI culture so default to culture value for both uiCultureName = cultureName; } - else if (cultureName == null && uiCultureName != null) + else if (cultureName.IsEmpty && !uiCultureName.IsEmpty) { // Value for UI culture but not for culture so default to UI culture value for both cultureName = uiCultureName; } - return new ProviderCultureResult(cultureName, uiCultureName); + return new ProviderCultureResult(cultureName.ToString(), uiCultureName.ToString()); } } diff --git a/src/Middleware/Rewrite/src/ApacheModRewrite/FileParser.cs b/src/Middleware/Rewrite/src/ApacheModRewrite/FileParser.cs index 9f6880b3984d..1f8f4b9758ac 100644 --- a/src/Middleware/Rewrite/src/ApacheModRewrite/FileParser.cs +++ b/src/Middleware/Rewrite/src/ApacheModRewrite/FileParser.cs @@ -12,9 +12,6 @@ public static IList Parse(TextReader input) var builder = new RuleBuilder(); var lineNum = 0; - // parsers - var flagsParser = new FlagParser(); - while ((line = input.ReadLine()) != null) { lineNum++; @@ -48,7 +45,7 @@ public static IList Parse(TextReader input) var flags = new Flags(); if (tokens.Count == 4) { - flags = flagsParser.Parse(tokens[3]); + flags = FlagParser.Parse(tokens[3]); } builder.AddConditionFromParts(pattern, condActionParsed, flags); @@ -67,7 +64,7 @@ public static IList Parse(TextReader input) Flags flags; if (tokens.Count == 4) { - flags = flagsParser.Parse(tokens[3]); + flags = FlagParser.Parse(tokens[3]); } else { diff --git a/src/Middleware/Rewrite/src/ApacheModRewrite/FlagParser.cs b/src/Middleware/Rewrite/src/ApacheModRewrite/FlagParser.cs index 59ac77affc4f..94e10419d64d 100644 --- a/src/Middleware/Rewrite/src/ApacheModRewrite/FlagParser.cs +++ b/src/Middleware/Rewrite/src/ApacheModRewrite/FlagParser.cs @@ -3,9 +3,9 @@ namespace Microsoft.AspNetCore.Rewrite.ApacheModRewrite; -internal sealed class FlagParser +internal static class FlagParser { - private readonly IDictionary _ruleFlagLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) { + private static readonly IDictionary _ruleFlagLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "b", FlagType.EscapeBackreference}, { "c", FlagType.Chain }, { "chain", FlagType.Chain}, @@ -52,7 +52,7 @@ internal sealed class FlagParser { "type", FlagType.Type }, }; - public Flags Parse(string flagString) + public static Flags Parse(string flagString) { if (string.IsNullOrEmpty(flagString)) { @@ -70,19 +70,21 @@ public Flags Parse(string flagString) // Invalid syntax to have any spaces. var tokens = flagString.Substring(1, flagString.Length - 2).Split(','); var flags = new Flags(); + Span hasPayload = stackalloc Range[3]; foreach (var token in tokens) { - var hasPayload = token.Split('='); + var tokenSpan = token.AsSpan(); + var length = tokenSpan.Split(hasPayload, '='); FlagType flag; - if (!_ruleFlagLookup.TryGetValue(hasPayload[0], out flag)) + if (!_ruleFlagLookup.TryGetValue(tokenSpan[hasPayload[0]].ToString(), out flag)) { - throw new FormatException($"Unrecognized flag: '{hasPayload[0]}'"); + throw new FormatException($"Unrecognized flag: '{tokenSpan[hasPayload[0]]}'"); } - if (hasPayload.Length == 2) + if (length == 2) { - flags.SetFlag(flag, hasPayload[1]); + flags.SetFlag(flag, tokenSpan[hasPayload[1]].ToString()); } else { diff --git a/src/Middleware/Rewrite/src/ApacheModRewrite/RuleBuilder.cs b/src/Middleware/Rewrite/src/ApacheModRewrite/RuleBuilder.cs index 4b35fde0bda3..2a2a6ac63ec7 100644 --- a/src/Middleware/Rewrite/src/ApacheModRewrite/RuleBuilder.cs +++ b/src/Middleware/Rewrite/src/ApacheModRewrite/RuleBuilder.cs @@ -36,7 +36,7 @@ public void AddRule(string rule) Flags flags; if (tokens.Count == 4) { - flags = new FlagParser().Parse(tokens[3]); + flags = FlagParser.Parse(tokens[3]); } else { diff --git a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs index 37baa577c20e..7a0841b1dd32 100644 --- a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs +++ b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.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; namespace Microsoft.AspNetCore.Rewrite; @@ -42,7 +43,7 @@ public static RewriteOptions Add(this RewriteOptions options, ActionIf the regex matches, what to replace the uri with. /// If the regex matches, conditionally stop processing other rules. /// The Rewrite options. - public static RewriteOptions AddRewrite(this RewriteOptions options, string regex, string replacement, bool skipRemainingRules) + public static RewriteOptions AddRewrite(this RewriteOptions options, [StringSyntax(StringSyntaxAttribute.Regex)] string regex, string replacement, bool skipRemainingRules) { options.Rules.Add(new RewriteRule(regex, replacement, skipRemainingRules)); return options; @@ -55,7 +56,7 @@ public static RewriteOptions AddRewrite(this RewriteOptions options, string rege /// The regex string to compare with. /// If the regex matches, what to replace the uri with. /// The Rewrite options. - public static RewriteOptions AddRedirect(this RewriteOptions options, string regex, string replacement) + public static RewriteOptions AddRedirect(this RewriteOptions options, [StringSyntax(StringSyntaxAttribute.Regex)] string regex, string replacement) { return AddRedirect(options, regex, replacement, statusCode: StatusCodes.Status302Found); } @@ -68,7 +69,7 @@ public static RewriteOptions AddRedirect(this RewriteOptions options, string reg /// If the regex matches, what to replace the uri with. /// The status code to add to the response. /// The Rewrite options. - public static RewriteOptions AddRedirect(this RewriteOptions options, string regex, string replacement, int statusCode) + public static RewriteOptions AddRedirect(this RewriteOptions options, [StringSyntax(StringSyntaxAttribute.Regex)] string regex, string replacement, int statusCode) { options.Rules.Add(new RedirectRule(regex, replacement, statusCode)); return options; diff --git a/src/Middleware/Rewrite/test/ApacheModRewrite/FlagParserTest.cs b/src/Middleware/Rewrite/test/ApacheModRewrite/FlagParserTest.cs index ea25a6344f74..2956ed151489 100644 --- a/src/Middleware/Rewrite/test/ApacheModRewrite/FlagParserTest.cs +++ b/src/Middleware/Rewrite/test/ApacheModRewrite/FlagParserTest.cs @@ -10,7 +10,7 @@ public class FlagParserTest [Fact] public void FlagParser_CheckSingleTerm() { - var results = new FlagParser().Parse("[NC]"); + var results = FlagParser.Parse("[NC]"); var dict = new Dictionary(); dict.Add(FlagType.NoCase, string.Empty); var expected = new Flags(dict); @@ -21,7 +21,7 @@ public void FlagParser_CheckSingleTerm() [Fact] public void FlagParser_CheckManyTerms() { - var results = new FlagParser().Parse("[NC,F,L]"); + var results = FlagParser.Parse("[NC,F,L]"); var dict = new Dictionary(); dict.Add(FlagType.NoCase, string.Empty); dict.Add(FlagType.Forbidden, string.Empty); @@ -34,7 +34,7 @@ public void FlagParser_CheckManyTerms() [Fact] public void FlagParser_CheckManyTermsWithEquals() { - var results = new FlagParser().Parse("[NC,F,R=301]"); + var results = FlagParser.Parse("[NC,F,R=301]"); var dict = new Dictionary(); dict.Add(FlagType.NoCase, string.Empty); dict.Add(FlagType.Forbidden, string.Empty); @@ -51,15 +51,15 @@ public void FlagParser_CheckManyTermsWithEquals() [InlineData("[RL]", "Unrecognized flag: 'RL'")] public void FlagParser_AssertFormatErrorWhenFlagsArePoorlyConstructed(string input, string expected) { - var ex = Assert.Throws(() => new FlagParser().Parse(input)); + var ex = Assert.Throws(() => FlagParser.Parse(input)); Assert.Equal(expected, ex.Message); } [Fact] public void FlagParser_AssertArgumentExceptionWhenFlagsAreNullOrEmpty() { - Assert.Throws(() => new FlagParser().Parse(null)); - Assert.Throws(() => new FlagParser().Parse(string.Empty)); + Assert.Throws(() => FlagParser.Parse(null)); + Assert.Throws(() => FlagParser.Parse(string.Empty)); } [Theory] @@ -68,7 +68,7 @@ public void FlagParser_AssertArgumentExceptionWhenFlagsAreNullOrEmpty() [InlineData("[CO=;NAME:VALUE;ABC:123]", ";NAME:VALUE;ABC:123")] public void Flag_ParserHandlesComplexFlags(string flagString, string expected) { - var results = new FlagParser().Parse(flagString); + var results = FlagParser.Parse(flagString); string value; Assert.True(results.GetValue(FlagType.Cookie, out value)); Assert.Equal(expected, value); diff --git a/src/Middleware/StaticFiles/src/HtmlDirectoryFormatter.cs b/src/Middleware/StaticFiles/src/HtmlDirectoryFormatter.cs index 449133a15be0..2cd66530ec05 100644 --- a/src/Middleware/StaticFiles/src/HtmlDirectoryFormatter.cs +++ b/src/Middleware/StaticFiles/src/HtmlDirectoryFormatter.cs @@ -103,7 +103,7 @@ header h1 {

{0} /", HtmlEncode(Resources.HtmlDir_IndexOf)); string cumulativePath = "/"; - foreach (var segment in requestPath.Value!.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var segment in requestPath.Value!.Split('/', StringSplitOptions.RemoveEmptyEntries)) { cumulativePath = cumulativePath + segment + "/"; builder.AppendFormat( diff --git a/src/Mvc/Mvc.Core/src/JsonOptions.cs b/src/Mvc/Mvc.Core/src/JsonOptions.cs index 834c758bb9dc..44c2522924f2 100644 --- a/src/Mvc/Mvc.Core/src/JsonOptions.cs +++ b/src/Mvc/Mvc.Core/src/JsonOptions.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -42,7 +43,7 @@ public class JsonOptions // The JsonSerializerOptions.GetTypeInfo method is called directly and needs a defined resolver // setting the default resolver (reflection-based) but the user can overwrite it directly or calling // .AddContext() - TypeInfoResolver = CreateDefaultTypeResolver() + TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver() }; private static IJsonTypeInfoResolver CreateDefaultTypeResolver() diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 477e4fb86aa1..161ce2ddcebf 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -33,6 +33,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + @@ -61,6 +62,10 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + + + + diff --git a/src/Mvc/Mvc.Core/src/Properties/ILLink.Substitutions.xml b/src/Mvc/Mvc.Core/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..9688f35dcd63 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs new file mode 100644 index 000000000000..05209836bd45 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/JsonOptionsTest.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.RemoteExecutor; + +namespace Microsoft.AspNetCore.Mvc; + +public class JsonOptionsTest +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_SetsTypeInfoResolverNull_WhenEnsureJsonTrimmabilityTrue() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", true.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions().JsonSerializerOptions; + + // Assert + Assert.Null(options.TypeInfoResolver); + }, options); + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void DefaultSerializerOptions_SetsTypeInfoResolverToDefault_WhenEnsureJsonTrimmabilityFalse() + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.EnsureJsonTrimmability", false.ToString()); + + using var remoteHandle = RemoteExecutor.Invoke(static () => + { + // Arrange + var options = new JsonOptions().JsonSerializerOptions; + + // Assert + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + }, options); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in index ead01179c249..b08f76ee56d6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in @@ -9,8 +9,6 @@ False true - full - false diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.Main.cs index 1e7082c4f9cb..dc6deae4de45 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.Main.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.Main.cs @@ -9,7 +9,7 @@ public class Program { public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(args); + var builder = WebApplication.CreateSlimBuilder(args); builder.Logging.AddConsole(); #if (NativeAot) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.cs index be0ffccde6fa..5f12bdd3c5ba 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Api-CSharp/Program.cs @@ -3,7 +3,7 @@ #endif using Company.ApiApplication1; -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateSlimBuilder(args); builder.Logging.AddConsole(); #if (NativeAot) diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 3f86c5b90f57..8c845e949d58 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -205,7 +205,7 @@ public long? ContentLength { // Note Http.Sys adds the Transfer-Encoding: chunked header to HTTP/2 requests with bodies for back compat. var transferEncoding = Headers[HeaderNames.TransferEncoding].ToString(); - if (transferEncoding != null && string.Equals("chunked", transferEncoding.Split(',')[^1].Trim(), StringComparison.OrdinalIgnoreCase)) + if (IsChunked(transferEncoding)) { _contentBoundaryType = BoundaryType.Chunked; } @@ -532,7 +532,7 @@ private void RemoveContentLengthIfTransferEncodingContainsChunked() if (StringValues.IsNullOrEmpty(Headers.ContentLength)) { return; } var transferEncoding = Headers[HeaderNames.TransferEncoding].ToString(); - if (transferEncoding == null || !string.Equals("chunked", transferEncoding.Split(',')[^1].Trim(), StringComparison.OrdinalIgnoreCase)) + if (!IsChunked(transferEncoding)) { return; } @@ -554,4 +554,19 @@ private void RemoveContentLengthIfTransferEncodingContainsChunked() headerDictionary.Add("X-Content-Length", headerDictionary[HeaderNames.ContentLength]); Headers.ContentLength = StringValues.Empty; } + + private static bool IsChunked(string? transferEncoding) + { + if (transferEncoding is null) + { + return false; + } + + var index = transferEncoding.LastIndexOf(','); + if (transferEncoding.AsSpan().Slice(index + 1).Trim().Equals("chunked", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index adde5fed6bbb..e7e3e2f81563 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -20,10 +20,10 @@ - - - - + + + + diff --git a/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h b/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h index bfe3225dbd6f..0645f4512651 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h @@ -16,7 +16,6 @@ #define ASPNETCORE_PORT_ENV_STR L"ASPNETCORE_PORT=" #define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" #define ASPNETCORE_APP_TOKEN_ENV_STR L"ASPNETCORE_TOKEN=" -#define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" class PROCESS_MANAGER; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index bb0d798f4889..df3417c5171c 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -345,7 +345,7 @@ private bool CheckRequestCanHaveBody() // Note Http.Sys adds the Transfer-Encoding: chunked header to HTTP/2 requests with bodies for back compat. // Transfer-Encoding takes priority over Content-Length. string transferEncoding = RequestHeaders.TransferEncoding.ToString(); - if (transferEncoding != null && string.Equals("chunked", transferEncoding.Split(',')[^1].Trim(), StringComparison.OrdinalIgnoreCase)) + if (IsChunked(transferEncoding)) { // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 // A sender MUST NOT send a Content-Length header field in any message @@ -830,4 +830,19 @@ private static NativeMethods.REQUEST_NOTIFICATION_STATUS ConvertRequestCompletio return success ? NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_CONTINUE : NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_FINISH_REQUEST; } + + private static bool IsChunked(string? transferEncoding) + { + if (transferEncoding is null) + { + return false; + } + + var index = transferEncoding.LastIndexOf(','); + if (transferEncoding.AsSpan().Slice(index + 1).Trim().Equals("chunked", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } } diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj index ba8561bb4cc2..fe4ad4ddcc9b 100644 --- a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -34,8 +34,8 @@ - - + + diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index ccfe2c28c585..8ab8fb711da1 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -17,10 +17,11 @@ + - - - + + + diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index cca8788dce80..99c9f591ee43 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -6,12 +6,12 @@ - - - - - - + + + + + + diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 693f8d4b23d6..5f05a46300ca 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -94,6 +94,15 @@ private async Task StartAsync(NamedPipeServerStream nextStream) } } } + catch (IOException ex) when (!_listeningToken.IsCancellationRequested) + { + // WaitForConnectionAsync can throw IOException when the pipe is broken. + NamedPipeLog.ConnectionListenerBrokenPipe(_log, ex); + + // Dispose existing pipe, create a new one and continue accepting. + nextStream.Dispose(); + nextStream = CreateServerStream(); + } catch (OperationCanceledException ex) when (_listeningToken.IsCancellationRequested) { // Cancelled the current token diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs index 0adc04481e1f..e02ad8bb9a6d 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs @@ -77,4 +77,6 @@ public static void ConnectionDisconnect(ILogger logger, NamedPipeConnection conn } } + [LoggerMessage(8, LogLevel.Debug, "Named pipe listener received broken pipe while waiting for a connection.", EventName = "ConnectionListenerBrokenPipe")] + public static partial void ConnectionListenerBrokenPipe(ILogger logger, Exception ex); } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/test/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj b/src/Servers/Kestrel/Transport.NamedPipes/test/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj index a43c3ef8bc96..768d76495f10 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/test/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj +++ b/src/Servers/Kestrel/Transport.NamedPipes/test/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj @@ -10,13 +10,13 @@ - + - + diff --git a/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj b/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj index 36733755bf7e..b7ba36177491 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj +++ b/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj @@ -14,14 +14,14 @@ - + - - + + diff --git a/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj b/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj index 6a651656c7b3..49066bf15747 100644 --- a/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj +++ b/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index a4aafc4d8e7d..ecc1447c1f7c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -24,11 +24,12 @@ + - - - - + + + + diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj index de9ea4a26f3c..e2aa7705c270 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj @@ -20,10 +20,10 @@ - + - - + + diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj index 4338b1ea87f8..5a39a90d9b4e 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj @@ -16,12 +16,12 @@ - + - + diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj index b128aa8aff69..3e8bcdf17ec0 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj @@ -17,7 +17,7 @@ - + @@ -25,7 +25,7 @@ - + diff --git a/src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs b/src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs index d903bf99c34d..6686b2c0d8c1 100644 --- a/src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs +++ b/src/Shared/ProblemDetails/HttpValidationProblemDetailsJsonConverter.cs @@ -23,6 +23,7 @@ public static HttpValidationProblemDetails ReadProblemDetails(ref Utf8JsonReader throw new JsonException("Unexpected end when reading JSON."); } + var objectTypeInfo = options.GetTypeInfo(typeof(object)); while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.ValueTextEquals(Errors.EncodedUtf8Bytes)) @@ -31,7 +32,7 @@ public static HttpValidationProblemDetails ReadProblemDetails(ref Utf8JsonReader } else { - ProblemDetailsJsonConverter.ReadValue(ref reader, problemDetails, options); + ProblemDetailsJsonConverter.ReadValue(ref reader, problemDetails, objectTypeInfo); } } diff --git a/src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs b/src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs index 13fa59608f2b..f72524daad9a 100644 --- a/src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs +++ b/src/Shared/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Http; @@ -25,9 +26,10 @@ public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConver throw new JsonException("Unexpected end when reading JSON."); } + var objectTypeInfo = options.GetTypeInfo(typeof(object)); while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - ReadValue(ref reader, problemDetails, options); + ReadValue(ref reader, problemDetails, objectTypeInfo); } if (reader.TokenType != JsonTokenType.EndObject) @@ -45,7 +47,7 @@ public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSeri writer.WriteEndObject(); } - internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonTypeInfo extensionDataTypeInfo) { if (TryReadStringProperty(ref reader, Type, out var propertyValue)) { @@ -79,14 +81,7 @@ internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, { var key = reader.GetString()!; reader.Read(); - ReadExtension(value, key, ref reader, options); - } - - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "ProblemDetails.Extensions is annotated to expose this warning to callers.")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "ProblemDetails.Extensions is annotated to expose this warning to callers.")] - static void ReadExtension(ProblemDetails problemDetails, string key, ref Utf8JsonReader reader, JsonSerializerOptions options) - { - problemDetails.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof(object), options); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, extensionDataTypeInfo); } } @@ -130,17 +125,17 @@ internal static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails v writer.WriteString(Instance, value.Instance); } - WriteExtensions(value, writer, options); - - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "ProblemDetails.Extensions is annotated to expose this warning to callers.")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "ProblemDetails.Extensions is annotated to expose this warning to callers.")] - static void WriteExtensions(ProblemDetails problemDetails, Utf8JsonWriter writer, JsonSerializerOptions options) + foreach (var kvp in value.Extensions) { - foreach (var kvp in problemDetails.Extensions) + writer.WritePropertyName(kvp.Key); + + if (kvp.Value is null) + { + writer.WriteNullValue(); + } + else { - writer.WritePropertyName(kvp.Key); - // When AOT is enabled, Serialize will only work with values specified on the JsonContext. - JsonSerializer.Serialize(writer, kvp.Value, kvp.Value?.GetType() ?? typeof(object), options); + JsonSerializer.Serialize(writer, kvp.Value, options.GetTypeInfo(kvp.Value.GetType())); } } } diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/.gitattributes b/src/Shared/TestCertificates/.gitattributes similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/.gitattributes rename to src/Shared/TestCertificates/.gitattributes diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/aspnetdevcert.pfx b/src/Shared/TestCertificates/aspnetdevcert.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/aspnetdevcert.pfx rename to src/Shared/TestCertificates/aspnetdevcert.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.client.ini b/src/Shared/TestCertificates/eku.client.ini similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.client.ini rename to src/Shared/TestCertificates/eku.client.ini diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.client.pfx b/src/Shared/TestCertificates/eku.client.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.client.pfx rename to src/Shared/TestCertificates/eku.client.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.code_signing.ini b/src/Shared/TestCertificates/eku.code_signing.ini similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.code_signing.ini rename to src/Shared/TestCertificates/eku.code_signing.ini diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.code_signing.pfx b/src/Shared/TestCertificates/eku.code_signing.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.code_signing.pfx rename to src/Shared/TestCertificates/eku.code_signing.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.multiple_usages.ini b/src/Shared/TestCertificates/eku.multiple_usages.ini similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.multiple_usages.ini rename to src/Shared/TestCertificates/eku.multiple_usages.ini diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.multiple_usages.pfx b/src/Shared/TestCertificates/eku.multiple_usages.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.multiple_usages.pfx rename to src/Shared/TestCertificates/eku.multiple_usages.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.server.ini b/src/Shared/TestCertificates/eku.server.ini similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.server.ini rename to src/Shared/TestCertificates/eku.server.ini diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/eku.server.pfx b/src/Shared/TestCertificates/eku.server.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/eku.server.pfx rename to src/Shared/TestCertificates/eku.server.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.crt b/src/Shared/TestCertificates/https-aspnet.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.crt rename to src/Shared/TestCertificates/https-aspnet.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.key b/src/Shared/TestCertificates/https-aspnet.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.key rename to src/Shared/TestCertificates/https-aspnet.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.pub b/src/Shared/TestCertificates/https-aspnet.pub similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.pub rename to src/Shared/TestCertificates/https-aspnet.pub diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-dsa-protected.key b/src/Shared/TestCertificates/https-dsa-protected.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-dsa-protected.key rename to src/Shared/TestCertificates/https-dsa-protected.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.crt b/src/Shared/TestCertificates/https-dsa.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.crt rename to src/Shared/TestCertificates/https-dsa.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.key b/src/Shared/TestCertificates/https-dsa.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.key rename to src/Shared/TestCertificates/https-dsa.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.pem b/src/Shared/TestCertificates/https-dsa.pem similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.pem rename to src/Shared/TestCertificates/https-dsa.pem diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa-protected.key b/src/Shared/TestCertificates/https-ecdsa-protected.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa-protected.key rename to src/Shared/TestCertificates/https-ecdsa-protected.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.crt b/src/Shared/TestCertificates/https-ecdsa.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.crt rename to src/Shared/TestCertificates/https-ecdsa.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.key b/src/Shared/TestCertificates/https-ecdsa.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.key rename to src/Shared/TestCertificates/https-ecdsa.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.pem b/src/Shared/TestCertificates/https-ecdsa.pem similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-ecdsa.pem rename to src/Shared/TestCertificates/https-ecdsa.pem diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-rsa-protected.key b/src/Shared/TestCertificates/https-rsa-protected.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-rsa-protected.key rename to src/Shared/TestCertificates/https-rsa-protected.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.crt b/src/Shared/TestCertificates/https-rsa.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.crt rename to src/Shared/TestCertificates/https-rsa.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.key b/src/Shared/TestCertificates/https-rsa.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.key rename to src/Shared/TestCertificates/https-rsa.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.pem b/src/Shared/TestCertificates/https-rsa.pem similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/https-rsa.pem rename to src/Shared/TestCertificates/https-rsa.pem diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/intermediate2_ca.crt b/src/Shared/TestCertificates/intermediate2_ca.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/intermediate2_ca.crt rename to src/Shared/TestCertificates/intermediate2_ca.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/intermediate2_ca.key b/src/Shared/TestCertificates/intermediate2_ca.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/intermediate2_ca.key rename to src/Shared/TestCertificates/intermediate2_ca.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/intermediate_ca.crt b/src/Shared/TestCertificates/intermediate_ca.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/intermediate_ca.crt rename to src/Shared/TestCertificates/intermediate_ca.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/intermediate_ca.key b/src/Shared/TestCertificates/intermediate_ca.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/intermediate_ca.key rename to src/Shared/TestCertificates/intermediate_ca.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/leaf.com.crt b/src/Shared/TestCertificates/leaf.com.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/leaf.com.crt rename to src/Shared/TestCertificates/leaf.com.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/leaf.com.key b/src/Shared/TestCertificates/leaf.com.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/leaf.com.key rename to src/Shared/TestCertificates/leaf.com.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/make-test-certs.sh b/src/Shared/TestCertificates/make-test-certs.sh similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/make-test-certs.sh rename to src/Shared/TestCertificates/make-test-certs.sh diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/no_extensions.ini b/src/Shared/TestCertificates/no_extensions.ini similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/no_extensions.ini rename to src/Shared/TestCertificates/no_extensions.ini diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/no_extensions.pfx b/src/Shared/TestCertificates/no_extensions.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/no_extensions.pfx rename to src/Shared/TestCertificates/no_extensions.pfx diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/root_ca.crt b/src/Shared/TestCertificates/root_ca.crt similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/root_ca.crt rename to src/Shared/TestCertificates/root_ca.crt diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/root_ca.key b/src/Shared/TestCertificates/root_ca.key similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/root_ca.key rename to src/Shared/TestCertificates/root_ca.key diff --git a/src/Servers/Kestrel/shared/test/TestCertificates/testCert.pfx b/src/Shared/TestCertificates/testCert.pfx similarity index 100% rename from src/Servers/Kestrel/shared/test/TestCertificates/testCert.pfx rename to src/Shared/TestCertificates/testCert.pfx diff --git a/src/Servers/Kestrel/shared/test/TestResources.cs b/src/Shared/TestResources.cs similarity index 100% rename from src/Servers/Kestrel/shared/test/TestResources.cs rename to src/Shared/TestResources.cs diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs b/src/Shared/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs similarity index 100% rename from src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs rename to src/Shared/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/MsQuicSupportedAttribute.cs b/src/Shared/TransportTestHelpers/MsQuicSupportedAttribute.cs similarity index 100% rename from src/Servers/Kestrel/shared/test/TransportTestHelpers/MsQuicSupportedAttribute.cs rename to src/Shared/TransportTestHelpers/MsQuicSupportedAttribute.cs diff --git a/src/Shared/TrimmingAppContextSwitches.cs b/src/Shared/TrimmingAppContextSwitches.cs new file mode 100644 index 000000000000..641512945289 --- /dev/null +++ b/src/Shared/TrimmingAppContextSwitches.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Internal; + +internal sealed class TrimmingAppContextSwitches +{ + private const string EnsureJsonTrimmabilityKey = "Microsoft.AspNetCore.EnsureJsonTrimmability"; + + internal static bool EnsureJsonTrimmability { get; } = AppContext.TryGetSwitch(EnsureJsonTrimmabilityKey, out var enabled) && enabled; +} diff --git a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java index 724377ca474d..63867379e49d 100644 --- a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java +++ b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java @@ -23,6 +23,8 @@ public class HttpHubConnectionBuilder { private Map headers; private TransportEnum transportEnum; private Action1 configureBuilder; + private long serverTimeout = HubConnection.DEFAULT_SERVER_TIMEOUT; + private long keepAliveInterval = HubConnection.DEFAULT_KEEP_ALIVE_INTERVAL; HttpHubConnectionBuilder(String url) { this.url = url; @@ -140,6 +142,28 @@ public HttpHubConnectionBuilder setHttpClientBuilderCallback(Action1 emptyArray = new ArrayList<>(); private static final int MAX_NEGOTIATE_ATTEMPTS = 100; @@ -49,8 +52,8 @@ public class HubConnection implements AutoCloseable { // These are all user-settable properties private String baseUrl; private List onClosedCallbackList; - private long keepAliveInterval = 15 * 1000; - private long serverTimeout = 30 * 1000; + private long keepAliveInterval = DEFAULT_KEEP_ALIVE_INTERVAL; + private long serverTimeout = DEFAULT_SERVER_TIMEOUT; private long handshakeResponseTimeout = 15 * 1000; // Private property, modified for testing @@ -120,7 +123,7 @@ Transport getTransport() { HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, HubProtocol protocol, Single accessTokenProvider, long handshakeResponseTimeout, Map headers, TransportEnum transportEnum, - Action1 configureBuilder) { + Action1 configureBuilder, long serverTimeout, long keepAliveInterval) { if (url == null || url.isEmpty()) { throw new IllegalArgumentException("A valid url is required."); } @@ -159,6 +162,9 @@ Transport getTransport() { this.headers = headers; this.skipNegotiate = skipNegotiate; + this.serverTimeout = serverTimeout; + this.keepAliveInterval = keepAliveInterval; + this.callback = (payload) -> ReceiveLoop(payload); } @@ -433,15 +439,14 @@ private Completable stop(String errorMessage) { this.state.unlock(); } - Completable stopTask = startTask.onErrorComplete().andThen(Completable.defer(() -> + CompletableSubject subject = CompletableSubject.create(); + startTask.onErrorComplete().subscribe(() -> { Completable stop = connectionState.transport.stop(); - stop.onErrorComplete().subscribe(); - return stop; - })); - stopTask.onErrorComplete().subscribe(); + stop.subscribe(() -> subject.onComplete(), e -> subject.onError(e)); + }); - return stopTask; + return subject; } private void ReceiveLoop(ByteBuffer payload) diff --git a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/WebSocketTransport.java b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/WebSocketTransport.java index a2ae6970d628..d63f8ca8651e 100644 --- a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/WebSocketTransport.java +++ b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/WebSocketTransport.java @@ -82,7 +82,9 @@ public void setOnClose(TransportOnClosedCallback onCloseCallback) { @Override public Completable stop() { - return webSocketClient.stop().doOnEvent(t -> logger.info("WebSocket connection stopped.")); + Completable stop = webSocketClient.stop(); + stop.onErrorComplete().subscribe(() -> logger.info("WebSocket connection stopped.")); + return stop; } void onClose(Integer code, String reason) { diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/HubConnectionTest.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/HubConnectionTest.java index 34cf8cd64a4a..67bd463d77ee 100644 --- a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/HubConnectionTest.java +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/HubConnectionTest.java @@ -314,7 +314,7 @@ public void removingMultipleHandlersWithOneCallToRemove() { Action action = () -> value.getAndUpdate((val) -> val + 1); Action secondAction = () -> { value.getAndUpdate((val) -> val + 2); - + complete.onComplete(); }; @@ -2797,14 +2797,11 @@ public void SkippingNegotiateDoesNotNegotiate() { .create("http://example") .withTransport(TransportEnum.WEBSOCKETS) .shouldSkipNegotiate(true) + .withHandshakeResponseTimeout(1) .withHttpClient(client) .build(); - try { - hubConnection.start().timeout(30, TimeUnit.SECONDS).blockingAwait(); - } catch (Exception e) { - assertEquals("WebSockets isn't supported in testing currently.", e.getMessage()); - } + assertThrows(RuntimeException.class, () -> hubConnection.start().timeout(30, TimeUnit.SECONDS).blockingAwait()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertFalse(negotiateCalled.get()); @@ -3962,4 +3959,67 @@ public void hubConnectionStopDuringConnecting() { assertTrue(close.blockingAwait(30, TimeUnit.SECONDS)); } + + @Test + public void serverTimeoutIsSetThroughBuilder() + { + long timeout = 60 * 1000; + HubConnection hubConnection = HubConnectionBuilder + .create("http://example.com") + .withServerTimeout(timeout) + .build(); + + assertEquals(timeout, hubConnection.getServerTimeout()); + } + + @Test + public void keepAliveIntervalIsSetThroughBuilder() + { + long interval = 60 * 1000; + HubConnection hubConnection = HubConnectionBuilder + .create("http://example.com") + .withKeepAliveInterval(interval) + .build(); + + assertEquals(interval, hubConnection.getKeepAliveInterval()); + } + + @Test + public void WebsocketStopLoggedOnce() { + try (TestLogger logger = new TestLogger(WebSocketTransport.class.getName())) { + AtomicBoolean negotiateCalled = new AtomicBoolean(false); + TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", + (req) -> { + negotiateCalled.set(true); + return Single.just(new HttpResponse(200, "", + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + }); + + HubConnection hubConnection = HubConnectionBuilder + .create("http://example") + .withTransport(TransportEnum.WEBSOCKETS) + .shouldSkipNegotiate(true) + .withHandshakeResponseTimeout(100) + .withHttpClient(client) + .build(); + + Completable startTask = hubConnection.start().timeout(30, TimeUnit.SECONDS); + hubConnection.stop().timeout(30, TimeUnit.SECONDS).blockingAwait(); + + assertThrows(RuntimeException.class, () -> startTask.blockingAwait()); + assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); + assertFalse(negotiateCalled.get()); + + ILoggingEvent[] logs = logger.getLogs(); + int count = 0; + for (ILoggingEvent iLoggingEvent : logs) { + if (iLoggingEvent.getFormattedMessage().startsWith("WebSocket connection stopped.")) { + count++; + } + } + + assertEquals(1, count); + } + } } diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestHttpClient.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestHttpClient.java index c66d9c8690bb..1eacfbb678b3 100644 --- a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestHttpClient.java +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestHttpClient.java @@ -70,7 +70,7 @@ public TestHttpClient on(String method, String url, TestHttpRequestHandler handl @Override public WebSocketWrapper createWebSocket(String url, Map headers) { - throw new RuntimeException("WebSockets isn't supported in testing currently."); + return new TestWebSocketWrapper(url, headers); } @Override diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestLogger.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestLogger.java index d97e1ee91279..e64c845ce12d 100644 --- a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestLogger.java +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/TestLogger.java @@ -43,6 +43,15 @@ public void append(ILoggingEvent event) { this.logger.addAppender(this.appender); } + public ILoggingEvent[] getLogs() { + lock.lock(); + try { + return list.toArray(new ILoggingEvent[0]); + } finally { + lock.unlock(); + } + } + public ILoggingEvent assertLog(String logMessage) { ILoggingEvent[] localList; lock.lock(); diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTestHttpClient.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTestHttpClient.java new file mode 100644 index 000000000000..0436105d62bc --- /dev/null +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTestHttpClient.java @@ -0,0 +1,68 @@ +package com.microsoft.signalr; + +import java.nio.ByteBuffer; +import java.util.Map; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; + +class WebSocketTestHttpClient extends HttpClient { + @Override + public Single send(HttpRequest request) { + return null; + } + + @Override + public Single send(HttpRequest request, ByteBuffer body) { + return null; + } + + @Override + public WebSocketWrapper createWebSocket(String url, Map headers) { + return new TestWebSocketWrapper(url, headers); + } + + @Override + public HttpClient cloneWithTimeOut(int timeoutInMilliseconds) { + return null; + } + + @Override + public void close() { + } +} + +class TestWebSocketWrapper extends WebSocketWrapper { + private WebSocketOnClosedCallback onClose; + + public TestWebSocketWrapper(String url, Map headers) + { + } + + @Override + public Completable start() { + return Completable.complete(); + } + + @Override + public Completable stop() { + if (onClose != null) { + onClose.invoke(null, ""); + } + return Completable.complete(); + } + + @Override + public Completable send(ByteBuffer message) { + return Completable.complete(); + } + + @Override + public void setOnReceive(OnReceiveCallBack onReceive) { + } + + @Override + public void setOnClose(WebSocketOnClosedCallback onClose) { + this.onClose = onClose; + } +} \ No newline at end of file diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTransportTest.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTransportTest.java index e2b4370c12be..1777ecbc6de9 100644 --- a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTransportTest.java +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTransportTest.java @@ -5,17 +5,11 @@ import static org.junit.jupiter.api.Assertions.*; -import java.nio.ByteBuffer; import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; -import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.Single; - class WebSocketTransportTest { @Test public void CanPassNullExitCodeToOnClosed() { @@ -28,61 +22,4 @@ public void CanPassNullExitCodeToOnClosed() { transport.stop(); assertTrue(closed.get()); } - - class WebSocketTestHttpClient extends HttpClient { - @Override - public Single send(HttpRequest request) { - return null; - } - - @Override - public Single send(HttpRequest request, ByteBuffer body) { - return null; - } - - @Override - public WebSocketWrapper createWebSocket(String url, Map headers) { - return new TestWrapper(); - } - - @Override - public HttpClient cloneWithTimeOut(int timeoutInMilliseconds) { - return null; - } - - @Override - public void close() { - } - } - - class TestWrapper extends WebSocketWrapper { - private WebSocketOnClosedCallback onClose; - - @Override - public Completable start() { - return Completable.complete(); - } - - @Override - public Completable stop() { - if (onClose != null) { - onClose.invoke(null, ""); - } - return Completable.complete(); - } - - @Override - public Completable send(ByteBuffer message) { - return null; - } - - @Override - public void setOnReceive(OnReceiveCallBack onReceive) { - } - - @Override - public void setOnClose(WebSocketOnClosedCallback onClose) { - this.onClose = onClose; - } - } } diff --git a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj index 12180a437d92..d48aac848c9d 100644 --- a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj +++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj @@ -40,6 +40,7 @@ --> +