From fa46bce6e9e12cd82c68651c1f0efbfdbc4e98df Mon Sep 17 00:00:00 2001 From: Simon Ferquel Date: Fri, 20 Jan 2023 09:56:59 +0100 Subject: [PATCH 01/24] [Named Pipe] Check for pipe broken on WaitConnectionAsync According to https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.waitforconnection?view=net-7.0#system-io-pipes-namedpipeserverstream-waitforconnection, WaitConnectionAsync can throw IOException when a pipe is broken. I am unable to write a test reproducing it, but I have seen it happen in the wild under high load with Unity's custom named pipe transport. Without this catch block, the server can unexpectedly stop accepting connections under high load. Signed-off-by: Simon Ferquel --- .../src/Internal/NamedPipeConnectionListener.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 693f8d4b23d6..892e369bce36 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -94,6 +94,12 @@ private async Task StartAsync(NamedPipeServerStream nextStream) } } } + catch (IOException) when (!_listeningToken.IsCancellationRequested) + { + // pipe is broken. Dispose existing pipe, create a new one and continue accepting + nextStream.Dispose(); + nextStream = CreateServerStream(); + } catch (OperationCanceledException ex) when (_listeningToken.IsCancellationRequested) { // Cancelled the current token From ed77f149a664d968057e7b225939e5b8dbcc7ebd Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Fri, 20 Jan 2023 19:30:22 +0200 Subject: [PATCH 02/24] Run test on Linux and Mac (#45642) * Run test on Linux and Mac * Use `ReplaceLineEndings` * Always use lf * Different approach * Revert * Doc --- .../test/Http/HeaderDictionaryAddTest.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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); } From 756073782f00e59e17ea401057cffd28fc5b21e9 Mon Sep 17 00:00:00 2001 From: Doug Bunting <6431421+dougbu@users.noreply.github.com> Date: Fri, 20 Jan 2023 10:43:39 -0800 Subject: [PATCH 03/24] Update milestone assignments for PRs (#46178) - rules would have assigned PRs to released milestones - note however that these rules don't presently seem to be working!! nits: - remove duplicate rule for release/6.0 PRs - remove rule for release/3.1 PRs since the release is no EOL --- .github/fabricbot.json | 84 +----------------------------------------- 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/.github/fabricbot.json b/.github/fabricbot.json index f6b29e5c7440..9405028f0f5b 100644 --- a/.github/fabricbot.json +++ b/.github/fabricbot.json @@ -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", @@ -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", @@ -3436,7 +3356,7 @@ { "name": "addMilestone", "parameters": { - "milestoneName": "6.0.13" + "milestoneName": "6.0.14" } } ] @@ -3476,7 +3396,7 @@ { "name": "addMilestone", "parameters": { - "milestoneName": "7.0.2" + "milestoneName": "7.0.3" } } ] From 40dd2305cf26b3d3a5fa85473b66edf0fd14e40b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 20 Jan 2023 11:00:43 -0800 Subject: [PATCH 04/24] Making `JsonOptions` AOT/Trimmer-safe with EnsureJsonTrimmability switch (#45886) * Adding EnsureJsonTrimmability switch * Set TypeResolver to null * Removing RUC/RDC attributes * Removing ProblemDetails.Extension RUC/RDC * Adding Test remote execution support * Adding jsonoptions tests * Update ProblemDetails.cs * Update HttpValidationProblemDetailsJsonConverter.cs --- eng/Dependencies.props | 1 + eng/Version.Details.xml | 4 ++ eng/Versions.props | 1 + .../src/ProblemDetails/ProblemDetails.cs | 10 +-- .../test/ProblemDetailsJsonConverterTest.cs | 66 +++++++++++++++++++ .../src/HttpRequestJsonExtensions.cs | 14 ++-- .../src/HttpResponseJsonExtensions.cs | 14 ++-- src/Http/Http.Extensions/src/JsonOptions.cs | 3 +- ...icrosoft.AspNetCore.Http.Extensions.csproj | 16 +++-- .../src/ProblemDetailsJsonContext.cs | 2 + .../src/Properties/ILLink.Substitutions.xml | 8 +++ .../Http.Extensions/test/JsonOptionsTests.cs | 47 +++++++++++++ .../DeveloperExceptionPageMiddlewareImpl.cs | 2 - src/Mvc/Mvc.Core/src/JsonOptions.cs | 3 +- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 5 ++ .../src/Properties/ILLink.Substitutions.xml | 8 +++ src/Mvc/Mvc.Core/test/JsonOptionsTest.cs | 46 +++++++++++++ ...tpValidationProblemDetailsJsonConverter.cs | 3 +- .../ProblemDetailsJsonConverter.cs | 33 ++++------ src/Shared/TrimmingAppContextSwitches.cs | 11 ++++ .../src/Microsoft.AspNetCore.Testing.csproj | 1 + .../RemoteExecutionSupportedAttribute.cs | 22 +++++++ 22 files changed, 268 insertions(+), 52 deletions(-) create mode 100644 src/Http/Http.Extensions/src/Properties/ILLink.Substitutions.xml create mode 100644 src/Http/Http.Extensions/test/JsonOptionsTests.cs create mode 100644 src/Mvc/Mvc.Core/src/Properties/ILLink.Substitutions.xml create mode 100644 src/Mvc/Mvc.Core/test/JsonOptionsTest.cs create mode 100644 src/Shared/TrimmingAppContextSwitches.cs create mode 100644 src/Testing/src/xunit/RemoteExecutionSupportedAttribute.cs 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..6953742073d3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -319,5 +319,9 @@ https://github.com/dotnet/arcade 1b04d6de502c4108ada6ea8e5ccefdc2ddc3ee7b + + https://github.com/dotnet/arcade + 000000 + diff --git a/eng/Versions.props b/eng/Versions.props index 14ba44a615eb..351207a0f0b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -137,6 +137,7 @@ 8.0.0-beta.23063.7 8.0.0-beta.23063.7 + 8.0.0-beta.23063.7 8.0.0-alpha.1.23062.2 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/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/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/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/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/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/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/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 @@ --> + False true - full - false From ebbf5eb5c8735134d5f0546a830f11ee9e873b82 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 20 Jan 2023 16:12:50 -0800 Subject: [PATCH 07/24] string.Split to Span.Split (#46098) --- .../Routing/src/Matching/HostMatcherPolicy.cs | 22 ++++++++++--------- .../Extensions.Stores/src/UserStoreBase.cs | 15 ++++++++++++- .../src/CookieRequestCultureProvider.cs | 22 +++++++++---------- .../src/ApacheModRewrite/FileParser.cs | 7 ++---- .../src/ApacheModRewrite/FlagParser.cs | 18 ++++++++------- .../src/ApacheModRewrite/RuleBuilder.cs | 2 +- .../test/ApacheModRewrite/FlagParserTest.cs | 14 ++++++------ .../StaticFiles/src/HtmlDirectoryFormatter.cs | 2 +- .../HttpSys/src/RequestProcessing/Request.cs | 19 ++++++++++++++-- .../IIS/IIS/src/Core/IISHttpContext.cs | 17 +++++++++++++- 10 files changed, 91 insertions(+), 47 deletions(-) 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/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/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/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/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/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; + } } From 8d6d88ec2251eedd09400368938bafd669a5c4f1 Mon Sep 17 00:00:00 2001 From: Artak <34246760+mkArtakMSFT@users.noreply.github.com> Date: Fri, 20 Jan 2023 16:17:41 -0800 Subject: [PATCH 08/24] Workaround fabricbot `addMielstone` task issue (#46195) It looks like the `addMilestone` task doesn't run when there is an existing milestone set. Clearing milestone before setting it to avoid this issue --- .github/fabricbot.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/fabricbot.json b/.github/fabricbot.json index 9405028f0f5b..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" } }, { @@ -3067,7 +3067,7 @@ { "name": "prTargetsBranch", "parameters": { - "branchName": "release/5.0" + "branchName": "release/7.0" } } ] @@ -3078,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" } }, { @@ -3353,6 +3353,10 @@ ], "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to release/6.0 branch", "actions": [ + { + "name": "removeMilestone", + "parameters": {} + }, { "name": "addMilestone", "parameters": { @@ -3393,6 +3397,10 @@ ], "taskName": "[Milestone Assignments] Assign Milestone to PRs merged to release/7.0 branch", "actions": [ + { + "name": "removeMilestone", + "parameters": {} + }, { "name": "addMilestone", "parameters": { From 1078da9afca142a5bc6632b66730689b6ba896fd Mon Sep 17 00:00:00 2001 From: Shreyas Jejurkar Date: Sat, 21 Jan 2023 05:53:12 +0530 Subject: [PATCH 09/24] feat : Add `StringSyntax` for regex parameters (#40589) * feat : Add `StringSyntax` for regex parameters * add missing using statement. * addressed PR feedback * Update RuleBuilder.cs * removed usage from test utils Co-authored-by: Javier Calvarro Nelson --- .../Routing/src/Constraints/RegexInlineRouteConstraint.cs | 5 ++++- src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) 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/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; From 9ec0753f61b7d80fd989e6e1d30f356cf85e1d37 Mon Sep 17 00:00:00 2001 From: Nick Stanton Date: Fri, 20 Jan 2023 16:27:26 -0800 Subject: [PATCH 10/24] Add DispatchExceptionAsync to ComponentBase (#46074) * Add DispatchExceptionAsync * add testing * Update src/Components/Components/src/ComponentBase.cs Co-authored-by: Steve Sanderson * Remove ref since ComponentBase doesn't have access * Simplify ErrorBoundary test cases * API feedback: make RenderHandle.DispatchExceptionAsync internal * Revert "API feedback: make RenderHandle.DispatchExceptionAsync internal" This reverts commit c15f5fe6925194e6f1a11a8893a5cea57c89d8da. * Add unit test Co-authored-by: Steve Sanderson --- .../Components/src/ComponentBase.cs | 13 ++++++++ .../Components/src/PublicAPI.Unshipped.txt | 2 ++ src/Components/Components/src/RenderHandle.cs | 12 ++++++++ .../Components/src/RenderTree/Renderer.cs | 3 ++ .../Components/test/RendererTest.cs | 30 +++++++++++++++++++ .../test/E2ETest/Tests/ErrorBoundaryTest.cs | 2 ++ .../ErrorBoundaryCases.razor | 19 ++++++++++++ 7 files changed, 81 insertions(+) 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/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")); + } } From 0a3a01b5b14e2eca0ba6794f845aef4f84bd1b93 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 20 Jan 2023 21:41:55 -0600 Subject: [PATCH 11/24] Allow minimal host to be created without default HostBuilder behavior (#46040) * Allow minimal host to be created without default HostBuilder behavior This adds a new Hosting API to reduce startup and app size, and ensures the default behavior is NativeAOT compatible. Fix #32485 * Use the new slim hosting API in the api template. Refactor the WebHostBuilder classes to share more code. --- .../src/PublicAPI.Unshipped.txt | 3 + src/DefaultBuilder/src/WebApplication.cs | 23 + .../src/WebApplicationBuilder.cs | 155 +++- src/DefaultBuilder/src/WebHost.cs | 14 +- .../WebApplicationTests.cs | 688 +++++++++++------- .../src/GenericHost/GenericWebHostBuilder.cs | 95 +-- .../src/GenericHost/SlimWebHostBuilder.cs | 91 +++ .../src/GenericHost/WebHostBuilderBase.cs | 107 +++ .../GenericHostWebHostBuilderExtensions.cs | 31 +- .../Hosting/src/PublicAPI.Unshipped.txt | 1 + .../content/Api-CSharp/Program.Main.cs | 2 +- .../content/Api-CSharp/Program.cs | 2 +- 12 files changed, 847 insertions(+), 365 deletions(-) create mode 100644 src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs create mode 100644 src/Hosting/Hosting/src/GenericHost/WebHostBuilderBase.cs 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/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/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) From 87144203fb2471183a5ed240a8ceb3f4836dfab0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 23 Jan 2023 08:34:24 +0800 Subject: [PATCH 12/24] Fix route syntax highlighting parameter color (#46208) --- src/Framework/App.Ref/src/CompatibilitySuppressions.xml | 2 +- .../App.Runtime/src/CompatibilitySuppressions.xml | 2 +- .../RouteEmbeddedLanguage/RoutePatternClassifier.cs | 7 ++++++- .../RouteEmbeddedLanguage/RoutePatternClassifierTests.cs | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) 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/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"); } From 02635ec69f9e7bceca804b402b9e86707e90c850 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:43:59 +0000 Subject: [PATCH 13/24] Update dependencies from https://github.com/dotnet/arcade build 20230117.5 (#46215) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 20 ++++++++++---------- eng/Versions.props | 6 +++--- global.json | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 6953742073d3..1a313c24cc99 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -302,26 +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 - 000000 + 3600aa80a01e90f38a7b86b9d7c1264e091aa5a8 diff --git a/eng/Versions.props b/eng/Versions.props index 351207a0f0b6..d4176dab7a80 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -135,9 +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.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 20b9d2ed6aa7..5f842cce2e37 100644 --- a/global.json +++ b/global.json @@ -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" } } From ac52b4d201bea20048c53820b81962b8467dc1bf Mon Sep 17 00:00:00 2001 From: Robin Sue Date: Mon, 23 Jan 2023 17:41:28 +0100 Subject: [PATCH 14/24] Delete duplicate define (#46202) The exact same thing exists 2 lines above that --- .../OutOfProcessRequestHandler/serverprocess.h | 1 - 1 file changed, 1 deletion(-) 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; From 73690575fd21834bda000f542c0d686427f113ae Mon Sep 17 00:00:00 2001 From: William Godbe Date: Mon, 23 Jan 2023 09:50:18 -0800 Subject: [PATCH 15/24] Don't depend on source-build job while it isn't running (#46220) --- .azure/pipelines/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 1c716b2d4066..bc4ff343a52a 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -831,7 +831,6 @@ stages: - MacOS_Test - Linux_Test - Helix_x64 - - Source_Build_Managed pool: name: NetCore1ESPool-Internal demands: ImageOverride -equals 1es-windows-2019 @@ -870,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 From 1a1ab568867f537c1c3c7b36b2ba315a567d8b0d Mon Sep 17 00:00:00 2001 From: Thays Grazia Date: Mon, 23 Jan 2023 15:08:02 -0300 Subject: [PATCH 16/24] [blazor][wasm][debug]Press alt-shift-d and open firefox debug tab attached to the blazor app (#46132) * Press alt-shift-d and open firefox debug tab attached to the blazor app * remove debugger.launch. * removing unrelated changes * Removing unnecessary changes on chrome debugging. * addressing @mkArtakMSFT comments * Addressing @mkArtakMSFT comments. * Addressing Steve comments, adding a console.warning message and remove the beautiful message, removed the Newtonsoft from the send message, todo: remove the Newtonsoft from receive message. * Completely removing newtonsoft usage as asked by steve. * Change warning message. --- .../Web.JS/src/Platform/Mono/MonoDebugger.ts | 11 +- .../Server/src/DebugProxyLauncher.cs | 25 +- .../Server/src/PublicAPI.Unshipped.txt | 1 + .../WebAssembly/Server/src/TargetPickerUi.cs | 218 ++++++++++++++++++ ...semblyNetDebugProxyAppBuilderExtensions.cs | 17 +- 5 files changed, 261 insertions(+), 11 deletions(-) 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}"); From 3261139765836f058e29b5a66db838752dac24c5 Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 23 Jan 2023 13:36:05 -0500 Subject: [PATCH 17/24] Move Kestrel shared source to src\Shared (#46112) --- ...AspNetCore.Server.HttpSys.FunctionalTests.csproj | 8 ++++---- .../IIS.FunctionalTests/IIS.FunctionalTests.csproj | 4 ++-- ...soft.AspNetCore.Server.Kestrel.Core.Tests.csproj | 7 ++++--- ...Microsoft.AspNetCore.Server.Kestrel.Tests.csproj | 12 ++++++------ ...Server.Kestrel.Transport.NamedPipes.Tests.csproj | 4 ++-- ...tCore.Server.Kestrel.Transport.Quic.Tests.csproj | 6 +++--- .../samples/HttpClientApp/HttpClientApp.csproj | 4 ++-- .../InMemory.FunctionalTests.csproj | 9 +++++---- .../Interop.FunctionalTests.csproj | 6 +++--- .../test/Sockets.BindTests/Sockets.BindTests.csproj | 4 ++-- .../Sockets.FunctionalTests.csproj | 4 ++-- .../test => Shared}/TestCertificates/.gitattributes | 0 .../TestCertificates/aspnetdevcert.pfx | Bin .../test => Shared}/TestCertificates/eku.client.ini | 0 .../test => Shared}/TestCertificates/eku.client.pfx | Bin .../TestCertificates/eku.code_signing.ini | 0 .../TestCertificates/eku.code_signing.pfx | Bin .../TestCertificates/eku.multiple_usages.ini | 0 .../TestCertificates/eku.multiple_usages.pfx | Bin .../test => Shared}/TestCertificates/eku.server.ini | 0 .../test => Shared}/TestCertificates/eku.server.pfx | Bin .../TestCertificates/https-aspnet.crt | Bin .../TestCertificates/https-aspnet.key | 0 .../TestCertificates/https-aspnet.pub | 0 .../TestCertificates/https-dsa-protected.key | 0 .../test => Shared}/TestCertificates/https-dsa.crt | Bin .../test => Shared}/TestCertificates/https-dsa.key | 0 .../test => Shared}/TestCertificates/https-dsa.pem | 0 .../TestCertificates/https-ecdsa-protected.key | 0 .../TestCertificates/https-ecdsa.crt | Bin .../TestCertificates/https-ecdsa.key | 0 .../TestCertificates/https-ecdsa.pem | 0 .../TestCertificates/https-rsa-protected.key | 0 .../test => Shared}/TestCertificates/https-rsa.crt | Bin .../test => Shared}/TestCertificates/https-rsa.key | 0 .../test => Shared}/TestCertificates/https-rsa.pem | 0 .../TestCertificates/intermediate2_ca.crt | 0 .../TestCertificates/intermediate2_ca.key | 0 .../TestCertificates/intermediate_ca.crt | 0 .../TestCertificates/intermediate_ca.key | 0 .../test => Shared}/TestCertificates/leaf.com.crt | 0 .../test => Shared}/TestCertificates/leaf.com.key | 0 .../TestCertificates/make-test-certs.sh | 0 .../TestCertificates/no_extensions.ini | 0 .../TestCertificates/no_extensions.pfx | Bin .../test => Shared}/TestCertificates/root_ca.crt | 0 .../test => Shared}/TestCertificates/root_ca.key | 0 .../test => Shared}/TestCertificates/testCert.pfx | Bin .../Kestrel/shared/test => Shared}/TestResources.cs | 0 .../HttpSysHttp3SupportedAttribute.cs | 0 .../MsQuicSupportedAttribute.cs | 0 51 files changed, 35 insertions(+), 33 deletions(-) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/.gitattributes (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/aspnetdevcert.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.client.ini (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.client.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.code_signing.ini (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.code_signing.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.multiple_usages.ini (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.multiple_usages.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.server.ini (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/eku.server.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-aspnet.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-aspnet.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-aspnet.pub (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-dsa-protected.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-dsa.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-dsa.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-dsa.pem (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-ecdsa-protected.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-ecdsa.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-ecdsa.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-ecdsa.pem (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-rsa-protected.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-rsa.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-rsa.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/https-rsa.pem (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/intermediate2_ca.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/intermediate2_ca.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/intermediate_ca.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/intermediate_ca.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/leaf.com.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/leaf.com.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/make-test-certs.sh (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/no_extensions.ini (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/no_extensions.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/root_ca.crt (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/root_ca.key (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestCertificates/testCert.pfx (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TestResources.cs (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs (100%) rename src/{Servers/Kestrel/shared/test => Shared}/TransportTestHelpers/MsQuicSupportedAttribute.cs (100%) 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/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/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/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 From 19e357be93525b2693f5ab397530597d1af76321 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:44:53 -0800 Subject: [PATCH 18/24] [main] (deps): Bump src/submodules/googletest (#46214) Bumps [src/submodules/googletest](https://github.com/google/googletest) from `356fc30` to `ec25eea`. - [Release notes](https://github.com/google/googletest/releases) - [Commits](https://github.com/google/googletest/compare/356fc301251378e0f6fa6aa794d73714202887ac...ec25eea8f8237cf86c30703f59747e42f34b6f75) --- updated-dependencies: - dependency-name: src/submodules/googletest dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/submodules/googletest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/submodules/googletest b/src/submodules/googletest index 356fc3012513..ec25eea8f823 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 356fc301251378e0f6fa6aa794d73714202887ac +Subproject commit ec25eea8f8237cf86c30703f59747e42f34b6f75 From 0e511783c72a88642cd8c6c5131dda387dc29b57 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada <114938397+surayya-MS@users.noreply.github.com> Date: Mon, 23 Jan 2023 21:00:46 +0100 Subject: [PATCH 19/24] Added withServerTimeout and withKeepAliveInterval to HubConnectionBuilder for java client (#46172) * Added withServerTimeout and withKeepAliveInterval to HubConnectionBuilder for java client --- .../signalr/HttpHubConnectionBuilder.java | 26 ++++++++++++++++++- .../com/microsoft/signalr/HubConnection.java | 12 ++++++--- .../microsoft/signalr/HubConnectionTest.java | 26 ++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) 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); } 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..53d11d3d972d 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(); }; @@ -3962,4 +3962,28 @@ 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()); + } } From c5a59c400acbe71a4f6dcbad793992c0e91b2665 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 23 Jan 2023 12:27:17 -0800 Subject: [PATCH 20/24] Implement RouteHandlerServices.Map and use in RequestDelegateGenerator (#46180) * Implement RouteHandlerServices.Map and use in RequestDelegateGenerator * Tweak generated code * Remove warnings from generated code * Fix docstring for RouteHandlerServices.Map * Add baseline tests and fix global:: issue * Clean up generated code * Add HTTP verb caching and fix usings * Use FQN and remove GeneratedCode attribute --- .../gen/RequestDelegateGenerator.cs | 43 +- .../gen/RequestDelegateGeneratorSources.cs | 449 +++++------------- .../StaticRouteHandlerModel.Emitter.cs | 21 +- ...ft.AspNetCore.Http.Extensions.Tests.csproj | 6 + ...aram_StringReturn_WithFilter.generated.txt | 183 +++++++ .../RequestDelegateGeneratorTestBase.cs | 45 +- .../RequestDelegateGeneratorTests.cs | 36 ++ .../Builder/EndpointRouteBuilderExtensions.cs | 2 +- .../src/Builder/RouteHandlerServices.cs | 56 +++ src/Http/Routing/src/PublicAPI.Unshipped.txt | 2 + 10 files changed, 476 insertions(+), 367 deletions(-) create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt create mode 100644 src/Http/Routing/src/Builder/RouteHandlerServices.cs 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/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/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! From 9a118ce20fb34a72a97a6394156a602098fcaa4d Mon Sep 17 00:00:00 2001 From: Adnan Date: Mon, 23 Jan 2023 16:41:49 -0500 Subject: [PATCH 21/24] Enumerate paramnames on startup (#46086) Enumerate endpoint parameter names on startup instead of allocating a list. --- .../src/RequestDelegateFactory.cs | 2 +- .../src/RequestDelegateFactoryContext.cs | 2 +- src/Http/Routing/src/RouteEndpointDataSource.cs | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) 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/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) From 351eccfd2ad0164353ef8ffced268fc762a818c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Mon, 23 Jan 2023 16:00:57 -0800 Subject: [PATCH 22/24] Update SDK (#46224) --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 5f842cce2e37..48c04c363db1 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.100-alpha.1.23070.2" + "version": "8.0.100-alpha.1.23073.1" }, "tools": { - "dotnet": "8.0.100-alpha.1.23070.2", + "dotnet": "8.0.100-alpha.1.23073.1", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" From 4b66d4052ac1ca25897eb18a5bc122917c5bd438 Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 23 Jan 2023 16:09:57 -0800 Subject: [PATCH 23/24] [SignalR] [Java] Log 'WebSocket stopped' once (#43532) --- .../com/microsoft/signalr/HubConnection.java | 11 ++- .../microsoft/signalr/WebSocketTransport.java | 4 +- .../microsoft/signalr/HubConnectionTest.java | 46 +++++++++++-- .../com/microsoft/signalr/TestHttpClient.java | 2 +- .../com/microsoft/signalr/TestLogger.java | 9 +++ .../signalr/WebSocketTestHttpClient.java | 68 +++++++++++++++++++ .../signalr/WebSocketTransportTest.java | 63 ----------------- 7 files changed, 127 insertions(+), 76 deletions(-) create mode 100644 src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/WebSocketTestHttpClient.java diff --git a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java index c306d592fd85..5b038e56ccf2 100644 --- a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java +++ b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java @@ -439,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 53d11d3d972d..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 @@ -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()); @@ -3986,4 +3983,43 @@ public void keepAliveIntervalIsSetThroughBuilder() 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; - } - } } From 7cb0ee92d195ac7655b349018bfef19823b52c61 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 24 Jan 2023 13:31:05 +0800 Subject: [PATCH 24/24] Add logging --- .../src/Internal/NamedPipeConnectionListener.cs | 7 +++++-- .../Transport.NamedPipes/src/Internal/NamedPipeLog.cs | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 892e369bce36..5f05a46300ca 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -94,9 +94,12 @@ private async Task StartAsync(NamedPipeServerStream nextStream) } } } - catch (IOException) when (!_listeningToken.IsCancellationRequested) + catch (IOException ex) when (!_listeningToken.IsCancellationRequested) { - // pipe is broken. Dispose existing pipe, create a new one and continue accepting + // 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(); } 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); }